Merge pull request #7304 from rmehta/milestone-tracker

feat: Milestone Tracker. Track document lifecycle with milestones
This commit is contained in:
Rushabh Mehta 2019-04-22 11:08:49 +05:30 committed by GitHub
commit 01242afc8f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 657 additions and 90 deletions

View file

@ -7,13 +7,14 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.desk.form import assign_to
import frappe.cache_manager
class AssignmentRule(Document):
def on_update(self): # pylint: disable=no-self-use
frappe.cache().delete_value('assignment_rule')
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.name)
def after_rename(self): # pylint: disable=no-self-use
frappe.cache().delete_value('assignment_rule')
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.name)
def apply_unassign(self, doc, assignments):
if (self.unassign_condition and
@ -113,14 +114,14 @@ def apply(doc, method):
if frappe.flags.in_patch or frappe.flags.in_install:
return
assignment_rules = frappe.cache().get_value('assignment_rule', get_assignment_rules)
assignment_rules = frappe.cache_manager.get_doctype_map('Assignment Rule', doc.doctype, dict(
document_type = doc.doctype, disabled = 0), order_by = 'priority desc')
assignment_rule_docs = []
# build rules
if doc.doctype in assignment_rules:
# multiple auto assigns
for d in frappe.db.get_all('Assignment Rule', dict(document_type=doc.doctype, disabled = 0), order_by = 'priority desc'):
assignment_rule_docs.append(frappe.get_doc('Assignment Rule', d.name))
# multiple auto assigns
for d in assignment_rules:
assignment_rule_docs.append(frappe.get_doc('Assignment Rule', d.name))
if not assignment_rule_docs:
return

View file

@ -0,0 +1,8 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Milestone', {
// refresh: function(frm) {
// }
});

View file

@ -0,0 +1,230 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "",
"beta": 0,
"creation": "2019-04-17 09:39:15.647817",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 0,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "reference_type",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Document Type",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 1,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "reference_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Document",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "track_field",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Track Field",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "value",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Value",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "milestone_tracker",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Milestone Tracker",
"length": 0,
"no_copy": 0,
"options": "Milestone Tracker",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_toolbar": 0,
"idx": 0,
"in_create": 1,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-04-17 16:01:21.430344",
"modified_by": "Administrator",
"module": "Automation",
"name": "Milestone",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "ASC",
"title_field": "reference_type",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

View file

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class Milestone(Document):
pass
def on_doctype_update():
frappe.db.add_index("Milestone", ["reference_type", "reference_name"])

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
#import frappe
import unittest
class TestMilestone(unittest.TestCase):
pass

View file

@ -0,0 +1,30 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Milestone Tracker', {
refresh: function(frm) {
frm.trigger('update_options');
},
update_options: function(frm) {
// update select options for `track_field`
let doctype = frm.doc.document_type;
let track_fields = [];
if (doctype) {
frappe.model.with_doctype(doctype, () => {
// get all date and datetime fields
frappe.get_meta(doctype).fields.map(df => {
if (['Link', 'Select'].includes(df.fieldtype)) {
track_fields.push({label: df.label, value: df.fieldname});
}
});
frm.set_df_property('track_field', 'options', track_fields);
});
} else {
// update select options
frm.set_df_property('track_field', 'options', []);
}
},
});

View file

@ -0,0 +1,161 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "format:{document_type}-{track_field}",
"beta": 0,
"creation": "2019-04-17 09:36:41.774774",
"custom": 0,
"description": "Track milestones for any document",
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 0,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "document_type",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Document Type to Track",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "track_field",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Field to Track",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "disabled",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Disabled",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_toolbar": 0,
"idx": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-04-17 14:48:29.510679",
"modified_by": "Administrator",
"module": "Automation",
"name": "Milestone Tracker",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

View file

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
import frappe.cache_manager
class MilestoneTracker(Document):
def on_update(self):
frappe.cache_manager.clear_doctype_map('Milestone Tracker', self.name)
def on_trash(self):
frappe.cache_manager.clear_doctype_map('Milestone Tracker', self.name)
def apply(self, doc):
before_save = doc.get_doc_before_save()
from_value = before_save and before_save.get(self.track_field) or None
if from_value != doc.get(self.track_field):
frappe.get_doc(dict(
doctype = 'Milestone',
reference_type = doc.doctype,
reference_name = doc.name,
track_field = self.track_field,
from_value = from_value,
value = doc.get(self.track_field),
milestone_tracker = self.name,
)).insert()
def evaluate_milestone(doc, event):
for d in frappe.cache_manager.get_doctype_map('Milestone Tracker', doc.doctype,
dict(document_type = doc.doctype, disabled=0)):
frappe.get_doc('Milestone Tracker', d.name).apply(doc)

View file

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
class TestMilestoneTracker(unittest.TestCase):
def test_milestone(self):
frappe.db.sql('delete from `tabMilestone Tracker`')
frappe.get_doc(dict(
doctype = 'Milestone Tracker',
document_type = 'ToDo',
track_field = 'status'
)).insert()
todo = frappe.get_doc(dict(
doctype = 'ToDo',
description = 'test milestone'
)).insert()
milestones = frappe.get_all('Milestone',
fields = ['track_field', 'value', 'milestone_tracker'],
filters = dict(reference_type = todo.doctype, reference_name=todo.name))
self.assertEqual(len(milestones), 1)
self.assertEqual(milestones[0].track_field, 'status')
self.assertEqual(milestones[0].value, 'Open')
todo.status = 'Closed'
todo.save()
milestones = frappe.get_all('Milestone',
fields = ['track_field', 'value', 'milestone_tracker'],
filters = dict(reference_type = todo.doctype, reference_name=todo.name),
order_by = 'modified desc')
self.assertEqual(len(milestones), 2)
self.assertEqual(milestones[0].track_field, 'status')
self.assertEqual(milestones[0].value, 'Closed')

View file

@ -3,31 +3,40 @@
from __future__ import unicode_literals
import frappe
import frappe, json
import frappe.defaults
from frappe.desk.notifications import (delete_notification_count_for,
clear_notifications)
common_default_keys = ["__default", "__global"]
def clear_user_cache(user=None):
cache = frappe.cache()
global_cache_keys = ("app_hooks", "installed_apps",
"app_modules", "module_app", "notification_config", 'system_settings',
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
'active_modules', 'assignment_rule')
groups = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
"defaults", "user_permissions", "home_page", "linked_with",
"desktop_icons", 'portal_menu_items')
doctype_cache_keys = ("meta", "form_meta", "table_columns", "last_modified",
"linked_doctypes", 'notifications', 'workflow' ,'energy_point_rule_map')
def clear_user_cache(user=None):
cache = frappe.cache()
# this will automatically reload the global cache
# so it is important to clear this first
clear_notifications(user)
if user:
for name in groups:
for name in user_cache_keys:
cache.hdel(name, user)
cache.delete_keys("user:" + user)
clear_defaults_cache(user)
else:
for name in groups:
for name in user_cache_keys:
cache.delete_key(name)
clear_defaults_cache()
clear_global_cache()
@ -37,10 +46,7 @@ def clear_global_cache():
clear_doctype_cache()
clear_website_cache()
frappe.cache().delete_value(["app_hooks", "installed_apps",
"app_modules", "module_app", "notification_config", 'system_settings',
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
'active_modules', 'assignment_rule'])
frappe.cache().delete_value(global_cache_keys)
frappe.setup_module_map()
def clear_defaults_cache(user=None):
@ -63,11 +69,8 @@ def clear_doctype_cache(doctype=None):
for key in ('is_table', 'doctype_modules'):
cache.delete_value(key)
groups = ["meta", "form_meta", "table_columns", "last_modified",
"linked_doctypes", 'notifications', 'workflow']
def clear_single(dt):
for name in groups:
for name in doctype_cache_keys:
cache.hdel(name, dt)
if doctype:
@ -84,9 +87,31 @@ def clear_doctype_cache(doctype=None):
else:
# clear all
for name in groups:
for name in doctype_cache_keys:
cache.delete_value(name)
# Clear all document's cache. To clear documents of a specific DocType document_cache should be restructured
clear_document_cache()
def get_doctype_map(doctype, name, filters, order_by=None):
cache = frappe.cache()
cache_key = frappe.scrub(doctype) + '_map'
doctype_map = cache.hget(cache_key, name)
if doctype_map:
# cached, return
items = json.loads(doctype_map)
else:
# non cached, build cache
try:
items = frappe.get_all(doctype, filters=filters, order_by = order_by)
cache.hset(cache_key, doctype, json.dumps(items))
except frappe.db.TableMissingError:
# executed from inside patch, ignore
items = []
return items
def clear_doctype_map(doctype, name):
cache_key = frappe.scrub(doctype) + '_map'
frappe.cache().hdel(cache_key, name)

View file

@ -40,6 +40,9 @@ def get_diff(old, new, for_child=False):
],
}'''
if not new:
return None
out = frappe._dict(changed = [], added = [], removed = [], row_changed = [])
for df in new.meta.fields:
if df.fieldtype in no_value_fields and df.fieldtype not in table_fields:

View file

@ -833,7 +833,7 @@ class Database(object):
"""Returns list of column names from given doctype."""
columns = self.get_db_table_columns('tab' + doctype)
if not columns:
raise self.ProgrammingError
raise self.TableMissingError
return columns
def has_column(self, doctype, column):

View file

@ -17,6 +17,7 @@ from frappe.database.mariadb.schema import MariaDBTable
class MariaDBDatabase(Database):
ProgrammingError = pymysql.err.ProgrammingError
TableMissingError = pymysql.err.ProgrammingError
OperationalError = pymysql.err.OperationalError
InternalError = pymysql.err.InternalError
SQLError = pymysql.err.ProgrammingError

View file

@ -21,6 +21,7 @@ psycopg2.extensions.register_type(DEC2FLOAT)
class PostgresDatabase(Database):
ProgrammingError = psycopg2.ProgrammingError
TableMissingError = psycopg2.ProgrammingError
OperationalError = psycopg2.OperationalError
InternalError = psycopg2.InternalError
SQLError = psycopg2.ProgrammingError

View file

@ -76,12 +76,17 @@ frappe.ui.form.on('Dashboard Chart', {
frm.trigger('render_filters_table');
} else {
if (frm.doc.chart_type==='Custom') {
frappe.xcall('frappe.desk.doctype.dashboard_chart_source.dashboard_chart_source.get_config', {name: frm.doc.source})
.then(config => {
frappe.dom.eval(config);
frm.filters = frappe.dashboards.chart_sources[frm.doc.source].filters;
frm.trigger('render_filters_table');
});
if (frm.doc.source) {
frappe.xcall('frappe.desk.doctype.dashboard_chart_source.dashboard_chart_source.get_config', {name: frm.doc.source})
.then(config => {
frappe.dom.eval(config);
frm.filters = frappe.dashboards.chart_sources[frm.doc.source].filters;
frm.trigger('render_filters_table');
});
} else {
frm.filters = [];
frm.trigger('render_filters_table');
}
} else {
// standard filters
if (frm.doc.document_type) {

View file

@ -103,10 +103,14 @@ def get_docinfo(doc=None, doctype=None, name=None):
"rating": get_feedback_rating(doc.doctype, doc.name),
"views": get_view_logs(doc.doctype, doc.name),
"energy_point_logs": get_point_logs(doc.doctype, doc.name),
"is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user),
"document_follow_enabled": frappe.db.get_value("User", frappe.session.user, "document_follow_notify")
"milestones": get_milestones(doc.doctype, doc.name),
"is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user)
}
def get_milestones(doctype, name):
return frappe.db.get_all('Milestone', fields = ['creation', 'owner', 'track_field', 'value'],
filters=dict(reference_type=doctype, reference_name=name))
def get_attachments(dt, dn):
return frappe.get_all("File", fields=["name", "file_name", "file_url", "is_private"],
filters = {"attached_to_name": dn, "attached_to_doctype": dt})

View file

@ -118,7 +118,8 @@ doc_events = {
"frappe.core.doctype.activity_log.feed.update_feed",
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions",
"frappe.automation.doctype.assignment_rule.assignment_rule.apply",
"frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points"
"frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points",
"frappe.automation.doctype.milestone_tracker.milestone_tracker.evaluate_milestone"
],
"after_rename": "frappe.desk.notifications.clear_doctype_notifications",
"on_cancel": [

View file

@ -117,6 +117,12 @@ class Document(BaseDocument):
# incorrect arguments. let's not proceed.
raise ValueError('Illegal arguments')
@staticmethod
def whitelist(f):
"""Decorator: Whitelist method to be called remotely via REST API."""
f.whitelisted = True
return f
def reload(self):
"""Reload document from database"""
self.load_from_db()
@ -370,13 +376,7 @@ class Document(BaseDocument):
(self.name, self.doctype, fieldname))
def get_doc_before_save(self):
if not getattr(self, '_doc_before_save', None):
try:
self._doc_before_save = frappe.get_doc(self.doctype, self.name)
except frappe.DoesNotExistError:
self._doc_before_save = None
frappe.clear_last_message()
return self._doc_before_save
return getattr(self, '_doc_before_save', None)
def set_new_name(self, force=False):
"""Calls `frappe.naming.se_new_name` for parent and child docs."""
@ -834,11 +834,6 @@ class Document(BaseDocument):
elif alert.event=='Method' and method == alert.method:
_evaluate_alert(alert)
@staticmethod
def whitelist(f):
f.whitelisted = True
return f
@whitelist.__func__
def _submit(self):
"""Submit the document. Sets `docstatus` = 1, then saves."""
@ -899,11 +894,12 @@ class Document(BaseDocument):
def load_doc_before_save(self):
'''Save load document from db before saving'''
self._doc_before_save = None
if not (self.is_new()
and (getattr(self.meta, 'track_changes', False)
or self.meta.get_set_only_once_fields()
or self.meta.get_workflow())):
self.get_doc_before_save()
if not self.is_new():
try:
self._doc_before_save = frappe.get_doc(self.doctype, self.name)
except frappe.DoesNotExistError:
self._doc_before_save = None
frappe.clear_last_message()
def run_post_save_methods(self):
"""Run standard methods after `INSERT` or `UPDATE`. Standard Methods are:
@ -1018,12 +1014,6 @@ class Document(BaseDocument):
if not frappe.flags.in_migrate:
follow_document(self.doctype, self.name, frappe.session.user)
@staticmethod
def whitelist(f):
"""Decorator: Whitelist method to be called remotely via REST API."""
f.whitelisted = True
return f
@staticmethod
def hook(f):
"""Decorator: Make method `hookable` (i.e. extensible by another app).

View file

@ -20,7 +20,7 @@ frappe.ui.form.DocumentFollow = class DocumentFollow {
render_sidebar() {
const docinfo = this.frm.get_docinfo();
const document_follow_enabled = docinfo && docinfo.document_follow_enabled;
const document_follow_enabled = frappe.boot.user.document_follow_notify;
const document_can_be_followed = frappe.get_meta(this.frm.doctype).track_changes;
if (frappe.session.user === 'Administrator'
|| !document_follow_enabled

View file

@ -163,6 +163,9 @@ frappe.ui.form.Timeline = class Timeline {
// append energy point logs
timeline = timeline.concat(this.get_energy_point_logs());
// append milestones
timeline = timeline.concat(this.get_milestones());
// sort
timeline
.filter(a => a.content)
@ -429,7 +432,6 @@ frappe.ui.form.Timeline = class Timeline {
if(c.communication_type == "Feedback"){
c.icon = "octicon octicon-comment-discussion"
c.rating_icons = frappe.render_template("rating_icons", {rating: c.rating, show_label: true})
c.color = "#f39c12"
} else {
c.icon = {
"Email": "octicon octicon-mail",
@ -439,12 +441,12 @@ frappe.ui.form.Timeline = class Timeline {
"Event": "fa fa-calendar",
"Meeting": "octicon octicon-briefcase",
"ToDo": "fa fa-check",
"Created": "octicon octicon-plus",
"Submitted": "octicon octicon-lock",
"Cancelled": "octicon octicon-x",
"Assigned": "octicon octicon-person",
"Assignment Completed": "octicon octicon-check",
"Comment": "octicon octicon-comment-discussion",
"Milestone": "octicon octicon-milestone",
"Workflow": "octicon octicon-git-branch",
"Label": "octicon octicon-tag",
"Attachment": "octicon octicon-cloud-upload",
@ -457,25 +459,6 @@ frappe.ui.form.Timeline = class Timeline {
"Reply": "octicon octicon-mail-reply"
}[c.comment_type || c.communication_medium]
c.color = {
"Email": "#3498db",
"Chat": "#3498db",
"Phone": "#3498db",
"SMS": "#3498db",
"Created": "#1abc9c",
"Submitted": "#1abc9c",
"Cancelled": "#c0392b",
"Assigned": "#f39c12",
"Assignment Completed": "#16a085",
"Comment": "#f39c12",
"Workflow": "#2c3e50",
"Label": "#2c3e50",
"Attachment": "#7f8c8d",
"Attachment Removed": "#eee",
"Relinked": "#16a085",
"Reply": "#8d99a6"
}[c.comment_type || c.communication_medium];
c.icon_fg = {
"Attachment Removed": "#333",
}[c.comment_type || c.communication_medium]
@ -528,6 +511,21 @@ frappe.ui.form.Timeline = class Timeline {
return energy_point_logs;
}
get_milestones() {
let milestones = this.frm.get_docinfo().milestones;
milestones.map(log => {
log.color = 'dark';
log.sender = log.owner;
log.comment_type = 'Milestone';
log.content = __('{0} changed {1} to {2}', [
frappe.user.full_name(log.owner).bold(),
frappe.meta.get_label(this.frm.doctype, log.track_field),
log.value.bold()]);
return log;
});
return milestones;
}
cast_comment_as_communication(c) {
c.sender = c.comment_email;
c.sender_full_name = c.comment_by;

View file

@ -1,4 +1,5 @@
<div class="media timeline-item {% if (data.user_content) { %} user-content {% } else { %} notification-content {% } %}" data-doctype="{{ data.doctype }}" data-name="{%= data.name %}" data-communication-type = "{{ data.communication_type }}">
<div class="media timeline-item {% if (data.user_content) { %} user-content {% } else { %} notification-content {% } %} {{ data.color || "" }}"
data-doctype="{{ data.doctype }}" data-name="{{ data.name }}" data-communication-type = "{{ data.communication_type }}">
{% if (data.user_content) { %}
<span class="pull-left avatar avatar-medium hidden-xs" style="margin-top: 1px">
{% if(data.user_info.image) { %}
@ -148,7 +149,7 @@
</div>
{% } else if(in_list(["Assignment Completed", "Assigned", "Shared",
"Unshared"], data.comment_type)) { %}
"Unshared", "Milestone"], data.comment_type)) { %}
<div class="small">
<i class="{%= data.icon %} fa-fw"></i>
{% if (data.timeline_doctype===data.frm.doc.doctype
@ -162,7 +163,7 @@
{% if(data.link_doctype && data.link_name) { %}
<a href="#Form/{%= data.link_doctype %}/{%= data.link_name %}">
{% } %}
{%= __(data.content) %}
{{ __(data.content) }}
{% if(data.link_doctype && data.link_name) { %}
</a>
{% } %}

View file

@ -574,6 +574,10 @@ h6.uppercase, .h6.uppercase {
.timeline-indicator();
}
.timeline-item.notification-content.dark::before {
background-color: @text-color;
}
.timeline-item .reply-link {
margin-left: 15px;
font-size: 12px;

View file

@ -4,11 +4,18 @@
from __future__ import unicode_literals
import frappe
import frappe.cache_manager
from frappe.model.document import Document
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled
from frappe.social.doctype.energy_point_log.energy_point_log import create_energy_points_log
class EnergyPointRule(Document):
def on_update(self):
frappe.cache_manager.clear_doctype_map('Energy Point Rule', self.name)
def on_trash(self):
frappe.cache_manager.clear_doctype_map('Energy Point Rule', self.name)
def apply(self, doc):
if frappe.safe_eval(self.condition, None, {'doc': doc.as_dict()}):
multiplier = 1
@ -41,14 +48,9 @@ class EnergyPointRule(Document):
def process_energy_points(doc, state):
if frappe.flags.in_patch or frappe.flags.in_install or not is_energy_point_enabled():
return
# TODO: cache properly
# energy_point_doctypes = frappe.cache().get_value('energy_point_doctypes', get_energy_point_doctypes)
# if doc.doctype in energy_point_doctypes:
rules = frappe.get_all('Energy Point Rule', filters={
'reference_doctype': doc.doctype,
'enabled': 1
})
for d in rules:
for d in frappe.cache_manager.get_doctype_map('Energy Point Rule', doc.doctype,
dict(reference_doctype = doc.doctype, enabled=1)):
frappe.get_doc('Energy Point Rule', d.name).apply(doc)
def get_energy_point_doctypes():

View file

@ -198,7 +198,8 @@ class UserPermissions:
def load_user(self):
d = frappe.db.sql("""select email, first_name, last_name, creation,
email_signature, user_type, language, background_style, background_image,
mute_sounds, send_me_a_copy from tabUser where name = %s""", (self.name,), as_dict=1)[0]
mute_sounds, send_me_a_copy, document_follow_notify
from tabUser where name = %s""", (self.name,), as_dict=1)[0]
if not self.can_read:
self.build_permissions()