diff --git a/frappe/hooks.py b/frappe/hooks.py index 348f1d283b..4a76ee075b 100755 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -153,7 +153,8 @@ scheduler_events = { "frappe.utils.scheduler.restrict_scheduler_events_if_dormant", "frappe.email.doctype.auto_email_report.auto_email_report.send_daily", "frappe.core.doctype.feedback_request.feedback_request.delete_feedback_request", - "frappe.core.doctype.activity_log.activity_log.clear_authentication_logs" + "frappe.core.doctype.activity_log.activity_log.clear_authentication_logs", + "frappe.utils.change_log.check_for_update" ], "daily_long": [ "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily", diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 999c970ebc..11973d4f2e 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -79,6 +79,8 @@ frappe.Application = Class.extend({ this.show_notes(); } + this.show_update_available(); + // listen to csrf_update frappe.realtime.on("csrf_generated", function(data) { // handles the case when a user logs in again from another tab @@ -468,6 +470,12 @@ frappe.Application = Class.extend({ }; }, + show_update_available: () => { + frappe.call({ + "method": "frappe.utils.change_log.show_update_popup" + }); + }, + setup_analytics: function() { if(window.mixpanel) { window.mixpanel.identify(frappe.session.user); diff --git a/frappe/utils/change_log.py b/frappe/utils/change_log.py index 322b241d03..ed9f02fd6d 100644 --- a/frappe/utils/change_log.py +++ b/frappe/utils/change_log.py @@ -7,6 +7,7 @@ import json, subprocess, os from semantic_version import Version import frappe from frappe.utils import cstr +import requests, shlex def get_change_log(user=None): if not user: user = frappe.session.user @@ -124,3 +125,72 @@ def get_app_last_commit_ref(app): shell=True).strip()[:7] except Exception as e: return '' + +def check_for_update(): + updates = frappe._dict(major=[], minor=[], patch=[]) + apps = get_versions() + + for app in apps: + # Check if repo remote is on github + remote_url = subprocess.check_output("cd ../apps/{} && git ls-remote --get-url".format(app), shell=True) + if "github.com" not in remote_url: + continue + + # Get latest version from github + if 'https' not in remote_url: + continue + + org_name = remote_url.split('/')[3] + r = requests.get('https://api.github.com/repos/{}/{}/releases'.format(org_name, app)) + if r.status_code == 200 and r.json(): + # 0 => latest release + github_version = Version(r.json()[0]['tag_name'].strip('v')) + else: + # In case of an improper response or if there are no releases + continue + + # Get local instance's current version or the app + instance_version = Version(apps[app]['version']) + # Compare and popup update message + for update_type in updates: + if github_version.__dict__[update_type] > instance_version.__dict__[update_type]: + updates[update_type].append(frappe._dict( + current_version = str(instance_version), + available_version = str(github_version), + org_name = org_name, + app_name = app, + title = apps[app]['title'], + )) + break + + update_message = "" + for update_type in updates: + release_links = "" + for app in updates[update_type]: + release_links += "{title}: v{available_version}
".format( + available_version = app.available_version, + org_name = app.org_name, + app_name = app.app_name, + title = app.title + ) + if release_links: + update_message += "New {} releases for the following apps are available:

{}
".format(update_type, release_links) + + # "update-message" will store the update message string + # "update-user-set" will be a set of users + if update_message: + update_message += "Please ask your system manager to update your instance" + cache = frappe.cache() + cache.set_value("update-message", update_message) + user_list = [x.name for x in frappe.get_all("User", filters={"enabled": True})] + cache.sadd("update-user-set", *user_list) + +@frappe.whitelist() +def show_update_popup(): + cache = frappe.cache() + user = frappe.session.user + + # Check if user is int the set of users to send update message to + if cache.sismember("update-user-set", user): + frappe.msgprint(cache.get_value("update-message"), title="New updates are available", indicator='green') + cache.srem("update-user-set", user) diff --git a/frappe/utils/redis_wrapper.py b/frappe/utils/redis_wrapper.py index 1fd8c10d13..05af4c99fb 100644 --- a/frappe/utils/redis_wrapper.py +++ b/frappe/utils/redis_wrapper.py @@ -200,4 +200,27 @@ class RedisWrapper(redis.Redis): except redis.exceptions.ConnectionError: return [] + def sadd(self, name, *values): + """Add a member/members to a given set""" + super(redis.Redis, self).sadd(self.make_key(name), *values) + + def srem(self, name, *values): + """Remove a specific member/list of members from the set""" + super(redis.Redis, self).srem(self.make_key(name), *values) + + def sismember(self, name, value): + """Returns True or False based on if a given value is present in the set""" + return super(redis.Redis, self).sismember(self.make_key(name), value) + + def spop(self, name): + """Removes and returns a random member from the set""" + return super(redis.Redis, self).spop(self.make_key(name)) + + def srandmember(self, name, count=None): + """Returns a random member from the set""" + return super(redis.Redis, self).srandmember(self.make_key(name)) + + def smembers(self, name): + """Return all members of the set""" + return super(redis.Redis, self).smembers(self.make_key(name))