From bae97cfed450597aba14a7ce444ea5b6ce442fa3 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Sat, 8 Oct 2016 11:11:36 +0530 Subject: [PATCH] Email Alert on any controller method (#2157) * [docs] typo * [email alert] now on any standard controller method * [minor] install customizations with intall; * [test] [fix] and truncate subject in email; * [fix] error log seen issue --- frappe/core/doctype/error_log/error_log.py | 8 ++- .../trigger-event-on-deletion-of-grid-row.md | 63 +++++++++---------- .../doctype/email_alert/email_alert.json | 33 +++++++++- .../email/doctype/email_alert/email_alert.py | 57 ++++++----------- frappe/email/receive.py | 4 +- frappe/hooks.py | 9 --- frappe/installer.py | 3 +- frappe/model/base_document.py | 5 ++ frappe/model/document.py | 57 ++++++++++++++++- frappe/model/meta.py | 3 +- frappe/modules/utils.py | 9 ++- frappe/public/js/frappe/misc/pretty_date.js | 4 +- frappe/website/router.py | 37 +++++------ 13 files changed, 181 insertions(+), 111 deletions(-) diff --git a/frappe/core/doctype/error_log/error_log.py b/frappe/core/doctype/error_log/error_log.py index 5feeb946c6..60e8a5f4df 100644 --- a/frappe/core/doctype/error_log/error_log.py +++ b/frappe/core/doctype/error_log/error_log.py @@ -9,9 +9,13 @@ from frappe.model.document import Document class ErrorLog(Document): def onload(self): if not self.seen: - self.seen = 1 - self.save() + self.db_set('seen', 1) + frappe.db.commit() def set_old_logs_as_seen(): + # set logs as seen frappe.db.sql("""update `tabError Log` set seen=1 where seen=0 and datediff(curdate(), creation) > 7""") + + # clear old logs + frappe.db.sql("""delete from `tabError Log` where datediff(curdate(), creation) > 30""") diff --git a/frappe/docs/user/en/guides/app-development/trigger-event-on-deletion-of-grid-row.md b/frappe/docs/user/en/guides/app-development/trigger-event-on-deletion-of-grid-row.md index e772bafcc2..2c7b0a3792 100755 --- a/frappe/docs/user/en/guides/app-development/trigger-event-on-deletion-of-grid-row.md +++ b/frappe/docs/user/en/guides/app-development/trigger-event-on-deletion-of-grid-row.md @@ -1,41 +1,38 @@ -To trigger an event when a row from a Child Table has been deleted (when user clicks on `delete` button), you need to add a handler the `fieldname_remove` event to Child Table, where fieldname is the fieldname of the Child Table in Parent Table declaration. - - For example: - - Assuming that your parent DocType is named `Item` has a Table Field linked to `Item Color` DocType with decloration name `color`. - +To trigger an event when a row from a Child Table has been deleted (when user clicks on `delete` button), you need to add a handler the `fieldname_remove` event to Child Table, where fieldname is the fieldname of the Child Table in Parent Table declaration. + + For example: + + Assuming that your parent DocType is named `Item` has a Table Field linked to `Item Color` DocType with decloration name `color`. + In order to "catch" the delete event: - - ```javascript - frappe.ui.form.on('Item Color', - color_remove: function(frm) { - // You code here - // If you console.log(frm.doc.color) you will get the remaining color list - } - ); - ``` - + + frappe.ui.form.on('Item Color', { + color_remove: function(frm) { + // You code here + // If you console.log(frm.doc.color) you will get the remaining color list + } + ); + The same process is used to trigger the add event (when user clicks on `add row` button): + + frappe.ui.form.on('Item Color', { + color_remove: function(frm) { + // You code here + // If you console.log(frm.doc.color) you will get the remaining color list + }, + color_add: function(frm) { + } + }); + + Notice that the handling is be made on Child DocType Table `form.ui.on` and not on Parent Doctype so a minimal full example is: + + ```javascript - frappe.ui.form.on('Item Color', - color_remove: function(frm) { - // Your code here - }, - color_add: function(frm) { - // Your code here - } - ); - ``` - - Notice that the handling is be made on Child DocType Table `form.ui.on` and not on Parent Doctype so a minimal full example is: - - - ```javascript frappe.ui.form.on('Item',{ - // Your client side handling for Item + // Your client side handling for Item }); - - frappe.ui.form.on('Item Color', + + frappe.ui.form.on('Item Color', { color_remove: function(frm) { // Deleting is triggered here } diff --git a/frappe/email/doctype/email_alert/email_alert.json b/frappe/email/doctype/email_alert/email_alert.json index 4563b2ce18..63dc0b2815 100755 --- a/frappe/email/doctype/email_alert/email_alert.json +++ b/frappe/email/doctype/email_alert/email_alert.json @@ -10,6 +10,7 @@ "doctype": "DocType", "document_type": "System", "editable_grid": 0, + "engine": "InnoDB", "fields": [ { "allow_on_submit": 0, @@ -208,7 +209,7 @@ "label": "Send Alert On", "length": 0, "no_copy": 0, - "options": "\nNew\nSave\nSubmit\nCancel\nDays After\nDays Before\nValue Change\nCustom", + "options": "\nNew\nSave\nSubmit\nCancel\nDays After\nDays Before\nValue Change\nMethod\nCustom", "permlevel": 0, "print_hide": 0, "print_hide_if_no_value": 0, @@ -219,6 +220,34 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:doc.event=='Method'", + "description": "Trigger on valid methods like \"before_insert\", \"after_update\", etc (will depend on the DocType selected)", + "fieldname": "method", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Trigger Method", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_on_submit": 0, "bold": 0, @@ -596,7 +625,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2016-10-02 14:44:50.428445", + "modified": "2016-10-07 01:07:17.504744", "modified_by": "Administrator", "module": "Email", "name": "Email Alert", diff --git a/frappe/email/doctype/email_alert/email_alert.py b/frappe/email/doctype/email_alert/email_alert.py index 1409244084..03aaa06da5 100755 --- a/frappe/email/doctype/email_alert/email_alert.py +++ b/frappe/email/doctype/email_alert/email_alert.py @@ -30,6 +30,7 @@ class EmailAlert(Document): self.validate_condition() def on_update(self): + frappe.cache().hdel('email_alerts', self.document_type) path = export_module_json(self, self.is_standard, self.module) if path: # js @@ -157,60 +158,42 @@ def trigger_daily_alerts(): trigger_email_alerts(None, "daily") def trigger_email_alerts(doc, method=None): - from jinja2 import TemplateError if frappe.flags.in_import or frappe.flags.in_patch: # don't send email alerts while syncing or patching return if method == "daily": - for alert in frappe.db.sql_list("""select name from `tabEmail Alert` where event in ('Days Before', 'Days After') and enabled=1"""): alert = frappe.get_doc("Email Alert", alert) for doc in alert.get_documents_for_today(): evaluate_alert(doc, alert, alert.event) - else: - if method in ("on_update", "validate") and doc.flags.in_insert: - # don't call email alerts multiple times for inserts - # on insert only "New" type alert must be called - return - - eevent = { - "on_update": "Save", - "after_insert": "New", - "validate": "Value Change", - "on_submit": "Submit", - "on_cancel": "Cancel", - }[method] - - for alert in frappe.db.sql_list("""select name from `tabEmail Alert` - where document_type=%s and event=%s and enabled=1""", (doc.doctype, eevent)): - try: - evaluate_alert(doc, alert, eevent) - except TemplateError: - frappe.throw(_("Error while evaluating Email Alert {0}. Please fix your template.").format(alert)) def evaluate_alert(doc, alert, event): - if isinstance(alert, basestring): - alert = frappe.get_doc("Email Alert", alert) + from jinja2 import TemplateError + try: + if isinstance(alert, basestring): + alert = frappe.get_doc("Email Alert", alert) - context = get_context(doc) + context = get_context(doc) - if alert.condition: - if not eval(alert.condition, context): - return + if alert.condition: + if not eval(alert.condition, context): + return - if event=="Value Change" and not doc.is_new(): - if doc.get(alert.value_changed) == frappe.db.get_value(doc.doctype, - doc.name, alert.value_changed): - return # value not changed + if event=="Value Change" and not doc.is_new(): + if doc.get(alert.value_changed) == frappe.db.get_value(doc.doctype, + doc.name, alert.value_changed): + return # value not changed - if event != "Value Change" and not doc.is_new(): - # reload the doc for the latest values & comments, - # except for validate type event. - doc = frappe.get_doc(doc.doctype, doc.name) + if event != "Value Change" and not doc.is_new(): + # reload the doc for the latest values & comments, + # except for validate type event. + doc = frappe.get_doc(doc.doctype, doc.name) - alert.send(doc) + alert.send(doc) + except TemplateError: + frappe.throw(_("Error while evaluating Email Alert {0}. Please fix your template.").format(alert)) def get_context(doc): return {"doc": doc, "nowdate": nowdate} diff --git a/frappe/email/receive.py b/frappe/email/receive.py index 238abd8976..39b970497f 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -283,7 +283,7 @@ class Email: self.subject = self.subject.decode(_subject[0][1]) else: # assume that the encoding is utf-8 - self.subject = self.subject.decode("utf-8") + self.subject = self.subject.decode("utf-8")[:140] if not self.subject: self.subject = "No Subject" @@ -360,7 +360,7 @@ class Email: return part.get_payload() def get_attachment(self, part): - charset = self.get_charset(part) + #charset = self.get_charset(part) fcontent = part.get_payload(decode=True) if fcontent: diff --git a/frappe/hooks.py b/frappe/hooks.py index 3c2414cc44..441252eab8 100755 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -95,22 +95,13 @@ standard_queries = { doc_events = { "*": { - "after_insert": "frappe.email.doctype.email_alert.email_alert.trigger_email_alerts", - "validate": [ - "frappe.email.doctype.email_alert.email_alert.trigger_email_alerts", - ], "on_update": [ "frappe.desk.notifications.clear_doctype_notifications", - "frappe.email.doctype.email_alert.email_alert.trigger_email_alerts", "frappe.core.doctype.communication.feed.update_feed" ], "after_rename": "frappe.desk.notifications.clear_doctype_notifications", - "on_submit": [ - "frappe.email.doctype.email_alert.email_alert.trigger_email_alerts", - ], "on_cancel": [ "frappe.desk.notifications.clear_doctype_notifications", - "frappe.email.doctype.email_alert.email_alert.trigger_email_alerts" ], "on_trash": "frappe.desk.notifications.clear_doctype_notifications" }, diff --git a/frappe/installer.py b/frappe/installer.py index bf7184de6b..c9be853dd3 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -17,6 +17,7 @@ from frappe.utils.fixtures import sync_fixtures from frappe.website import render from frappe.desk.doctype.desktop_icon.desktop_icon import sync_from_app from frappe.utils.password import create_auth_table +from frappe.modules.utils import sync_customizations def install_db(root_login="root", root_password=None, db_name=None, source_sql=None, admin_password=None, verbose=True, force=0, site_config=None, reinstall=False): @@ -142,8 +143,8 @@ def install_app(name, verbose=False, set_as_patched=True): for after_install in app_hooks.after_install or []: frappe.get_attr(after_install)() - print "Installing fixtures..." sync_fixtures(name) + sync_customizations(name) frappe.flags.in_install = False diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index f9e954ee0d..1346f19250 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -273,6 +273,11 @@ class BaseDocument(object): if not self.name: # name will be set by document class in most cases set_new_name(self) + + if not self.creation: + self.creation = self.modified = now() + self.created_by = self.modifield_by = frappe.session.user + d = self.get_valid_dict() columns = d.keys() try: diff --git a/frappe/model/document.py b/frappe/model/document.py index 2ba5613a77..1f783cbcad 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -193,6 +193,8 @@ class Document(BaseDocument): if self.flags.in_print: return + self.flags.email_alerts_executed = [] + if ignore_permissions!=None: self.flags.ignore_permissions = ignore_permissions @@ -252,6 +254,8 @@ class Document(BaseDocument): if self.flags.in_print: return + self.flags.email_alerts_executed = [] + if ignore_permissions!=None: self.flags.ignore_permissions = ignore_permissions @@ -658,7 +662,54 @@ class Document(BaseDocument): fn = lambda self, *args, **kwargs: None fn.__name__ = method.encode("utf-8") - return Document.hook(fn)(self, *args, **kwargs) + out = Document.hook(fn)(self, *args, **kwargs) + + self.run_email_alerts(method) + + return out + + def run_email_alerts(self, method): + '''Run email alerts for this method''' + if frappe.flags.in_import or frappe.flags.in_patch or frappe.flags.in_install: + return + + if self.flags.email_alerts_executed==None: + self.flags.email_alerts_executed = [] + + from frappe.email.doctype.email_alert.email_alert import evaluate_alert + + if self.flags.email_alerts == None: + alerts = frappe.cache().hget('email_alerts', self.doctype) + if alerts==None: + alerts = frappe.get_all('Email Alert', fields=['name', 'event', 'method'], + filters={'enabled': 1, 'document_type': self.doctype}) + frappe.cache().hset('email_alerts', self.doctype, alerts) + self.flags.email_alerts = alerts + + if not self.flags.email_alerts: + return + + def _evaluate_alert(alert): + if not alert.name in self.flags.email_alerts_executed: + evaluate_alert(self, alert.name, alert.event) + + event_map = { + "on_update": "Save", + "after_insert": "New", + "on_submit": "Submit", + "on_cancel": "Cancel" + } + + if not self.flags.in_insert: + # value change is not applicable in insert + event_map['validate'] = 'Value Change' + + for alert in self.flags.email_alerts: + event = event_map.get(method, None) + if event and alert.event == event: + _evaluate_alert(alert) + elif alert.event=='Method' and method == alert.method: + _evaluate_alert(alert) @staticmethod def whitelist(f): @@ -1000,12 +1051,12 @@ def execute_action(doctype, name, action, **kwargs): getattr(doc, action)(**kwargs) except Exception: frappe.db.rollback() - + # add a comment (?) if frappe.local.message_log: msg = json.loads(frappe.local.message_log[-1]).get('message') else: msg = '
' + frappe.get_traceback() + '
' - + doc.add_comment('Comment', _('Action Failed') + '

' + msg) doc.notify_update() diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 04de16d88f..ed456898b3 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -415,7 +415,8 @@ def clear_cache(doctype=None): for key in ('is_table', 'doctype_modules'): cache.delete_value(key) - groups = ["meta", "form_meta", "table_columns", "last_modified", "linked_doctypes"] + groups = ["meta", "form_meta", "table_columns", "last_modified", + "linked_doctypes", 'email_alerts'] def clear_single(dt): for name in groups: diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py index 018b4e0796..35ff2b4691 100644 --- a/frappe/modules/utils.py +++ b/frappe/modules/utils.py @@ -71,10 +71,15 @@ def export_customizations(module, doctype, sync_on_migrate=0): frappe.msgprint('Customizations exported to {0}'.format(path)) -def sync_customizations(): +def sync_customizations(app=None): '''Sync custom fields and property setters from custom folder in each app module''' - for app_name in frappe.get_installed_apps(): + if app: + apps = [app] + else: + apps = frappe.get_installed_apps() + + for app_name in apps: for module_name in frappe.local.app_modules.get(app_name) or []: folder = frappe.get_app_path(app_name, module_name, 'custom') diff --git a/frappe/public/js/frappe/misc/pretty_date.js b/frappe/public/js/frappe/misc/pretty_date.js index 8146ed506b..2ea2b8dddc 100644 --- a/frappe/public/js/frappe/misc/pretty_date.js +++ b/frappe/public/js/frappe/misc/pretty_date.js @@ -1,5 +1,7 @@ function prettyDate(time, mini){ - + if(!time) { + time = new Date(); + } if(moment) { if(window.sys_defaults && sys_defaults.time_zone) { var ret = moment.tz(time, sys_defaults.time_zone).fromNow(mini); diff --git a/frappe/website/router.py b/frappe/website/router.py index 0a166d5d88..aeb00f20a4 100644 --- a/frappe/website/router.py +++ b/frappe/website/router.py @@ -8,6 +8,23 @@ from frappe.website.utils import can_cache, delete_page_cache from frappe.model.document import get_controller from frappe import _ +def resolve_route(path): + """Returns the page route object based on searching in pages and generators. + The `www` folder is also a part of generator **Web Page**. + + The only exceptions are `/about` and `/contact` these will be searched in Web Pages + first before checking the standard pages.""" + if path not in ("about", "contact"): + context = get_page_context_from_template(path) + if context: + return context + return get_page_context_from_doctype(path) + else: + context = get_page_context_from_doctype(path) + if context: + return context + return get_page_context_from_template(path) + def get_page_context(path): page_context = None if can_cache(): @@ -34,26 +51,9 @@ def make_page_context(path): return context -def resolve_route(path): - """Returns the page route object based on searching in pages and generators. - The `www` folder is also a part of generator **Web Page**. - - The only exceptions are `/about` and `/contact` these will be searched in Web Pages - first before checking the standard pages.""" - if path not in ("about", "contact"): - context = get_page_context_from_template(path) - if context: - return context - return get_page_context_from_doctype(path) - else: - context = get_page_context_from_doctype(path) - if context: - return context - return get_page_context_from_template(path) - def get_page_context_from_template(path): '''Return page_info from path''' - for app in frappe.get_installed_apps(): + for app in frappe.get_installed_apps(frappe_last=True): app_path = frappe.get_app_path(app) folders = frappe.local.flags.web_pages_folders or ('www', 'templates/pages') @@ -188,6 +188,7 @@ def get_page_info(path, app, basepath=None, app_path=None, fname=None): if os.path.exists(page_info.controller_path): controller = app + "." + os.path.relpath(page_info.controller_path, app_path).replace(os.path.sep, ".")[:-3] + page_info.controller = controller # get the source