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:
parent
3730e4fa8f
commit
bae97cfed4
13 changed files with 181 additions and 111 deletions
|
|
@ -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""")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue