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
This commit is contained in:
Rushabh Mehta 2016-10-08 11:11:36 +05:30 committed by GitHub
parent 3730e4fa8f
commit bae97cfed4
13 changed files with 181 additions and 111 deletions

View file

@ -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""")

View file

@ -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
}

View file

@ -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",

View file

@ -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}

View file

@ -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:

View file

@ -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"
},

View file

@ -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

View file

@ -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:

View file

@ -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 = '<pre><code>' + frappe.get_traceback() + '</pre></code>'
doc.add_comment('Comment', _('Action Failed') + '<br><br>' + msg)
doc.notify_update()

View file

@ -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:

View file

@ -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')

View file

@ -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);

View file

@ -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