feat: Document subscription (#6745)
This commit is contained in:
parent
5a67c57171
commit
cd191439fd
30 changed files with 966 additions and 27 deletions
|
|
@ -1176,6 +1176,107 @@
|
|||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 1,
|
||||
"columns": 0,
|
||||
"depends_on": "",
|
||||
"fieldname": "document_follow_notifications_section",
|
||||
"fieldtype": "Section Break",
|
||||
"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": "Document Follow",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "document_follow_notify",
|
||||
"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": "Send Notifications for documents followed by me",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "Daily",
|
||||
"depends_on": "eval:(doc.document_follow_notify== 1)",
|
||||
"fieldname": "document_follow_frequency",
|
||||
"fieldtype": "Select",
|
||||
"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": "Frequency",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Hourly\nDaily\nWeekly",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
|
|
@ -1226,7 +1327,7 @@
|
|||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Send Notifications for Transactions I Follow",
|
||||
"label": "Send Notifications for Email threads",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
|
|
@ -1676,7 +1777,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "Enter default value fields (keys) and values. If you add multiple values for a field,the first one will be picked. These defaults are also used to set \"match\" permission rules. To see list of fields,go to \"Customize Form\".",
|
||||
"description": "Enter default value fields (keys) and values. If you add multiple values for a field, the first one will be picked. These defaults are also used to set \"match\" permission rules. To see list of fields, go to \"Customize Form\".",
|
||||
"fieldname": "defaults",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 1,
|
||||
|
|
@ -1776,7 +1877,7 @@
|
|||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "System User",
|
||||
"description": "If the user has any role checked,then the user becomes a \"System User\". \"System User\" has access to the desktop",
|
||||
"description": "If the user has any role checked, then the user becomes a \"System User\". \"System User\" has access to the desktop",
|
||||
"fieldname": "user_type",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
|
|
@ -1908,7 +2009,7 @@
|
|||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "eval:doc.restrict_ip && doc.restrict_ip.length",
|
||||
"description": "If enabled, user can login from any IP Address using Two Factor Auth, this can also be set for all users in System Settings",
|
||||
"description": "If enabled, user can login from any IP Address using Two Factor Auth, this can also be set for all users in System Settings",
|
||||
"fieldname": "bypass_restrict_ip_check_if_2fa_enabled",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from __future__ import unicode_literals
|
|||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.desk.form.document_follow import follow_document
|
||||
import frappe.share
|
||||
|
||||
class DuplicateToDoError(frappe.ValidationError): pass
|
||||
|
|
@ -42,7 +43,6 @@ def add(args=None):
|
|||
AND `status`='Open'
|
||||
AND `owner`=%(assign_to)s""", args):
|
||||
frappe.throw(_("Already in user's To Do list"), DuplicateToDoError)
|
||||
|
||||
else:
|
||||
from frappe.utils import nowdate
|
||||
|
||||
|
|
@ -75,6 +75,9 @@ def add(args=None):
|
|||
frappe.share.add(doc.doctype, doc.name, args['assign_to'])
|
||||
frappe.msgprint(_('Shared with user {0} with read access').format(args['assign_to']), alert=True)
|
||||
|
||||
# make this document followed by assigned user
|
||||
follow_document(args['doctype'], args['name'], args['assign_to'])
|
||||
|
||||
# notify
|
||||
notify_assignment(d.assigned_by, d.owner, d.reference_type, d.reference_name, action='ASSIGN',\
|
||||
description=args.get("description"), notify=args.get('notify'))
|
||||
|
|
|
|||
280
frappe/desk/form/document_follow.py
Normal file
280
frappe/desk/form/document_follow.py
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import frappe.utils
|
||||
from frappe.utils import get_url_to_form
|
||||
from frappe import _
|
||||
from itertools import groupby
|
||||
|
||||
@frappe.whitelist()
|
||||
def follow_document(doctype, doc_name, user, force=False):
|
||||
'''
|
||||
param:
|
||||
Doctype name
|
||||
doc name
|
||||
user email
|
||||
|
||||
condition:
|
||||
avoided for some doctype
|
||||
follow only if track changes are set to 1
|
||||
'''
|
||||
avoid_follow = ["Communication", "ToDo", "DocShare", "Email Unsubscribe", "Activity Log",
|
||||
"File", "Version", "View Log", "Document Follow", "Comment"]
|
||||
|
||||
track_changes = frappe.get_meta(doctype).track_changes
|
||||
exists = is_document_followed(doctype, doc_name, user)
|
||||
if exists == 0:
|
||||
user_can_follow = frappe.db.get_value("User", user, "document_follow_notify")
|
||||
if user != "Administrator" and user_can_follow and track_changes and (doctype not in avoid_follow or force):
|
||||
doc = frappe.new_doc("Document Follow")
|
||||
doc.update({
|
||||
"ref_doctype": doctype,
|
||||
"ref_docname": doc_name,
|
||||
"user": user
|
||||
})
|
||||
doc.save()
|
||||
return doc
|
||||
|
||||
@frappe.whitelist()
|
||||
def unfollow_document(doctype, doc_name, user):
|
||||
doc = frappe.get_all(
|
||||
"Document Follow",
|
||||
filters={
|
||||
"ref_doctype": doctype,
|
||||
"ref_docname": doc_name,
|
||||
"user": user
|
||||
},
|
||||
fields=["name"],
|
||||
limit=1
|
||||
)
|
||||
if doc:
|
||||
frappe.delete_doc("Document Follow", doc[0].name)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def get_message(doc_name, doctype, frequency):
|
||||
activity_list = get_version(doctype, doc_name, frequency) + get_comments(doctype, doc_name, frequency)
|
||||
return sorted(activity_list, key=lambda k: k["time"], reverse=True)
|
||||
|
||||
def send_email_alert(receiver, docinfo, timeline):
|
||||
if receiver:
|
||||
frappe.sendmail(
|
||||
subject=_("Document Follow Notification"),
|
||||
recipients=[receiver],
|
||||
template="document_follow",
|
||||
args={
|
||||
"docinfo": docinfo,
|
||||
"timeline": timeline,
|
||||
}
|
||||
)
|
||||
|
||||
def send_document_follow_mails(frequency):
|
||||
|
||||
'''
|
||||
param:
|
||||
frequency for sanding mails
|
||||
|
||||
task:
|
||||
set receiver according to frequency
|
||||
group document list according to user
|
||||
get changes, activity, comments on doctype
|
||||
call method to send mail
|
||||
'''
|
||||
|
||||
users = frappe.get_list("Document Follow",
|
||||
fields=["*"])
|
||||
|
||||
sorted_users = sorted(users, key=lambda k: k['user'])
|
||||
|
||||
grouped_by_user = {}
|
||||
for k, v in groupby(sorted_users, key=lambda k: k['user']):
|
||||
grouped_by_user[k] = list(v)
|
||||
|
||||
for user in grouped_by_user:
|
||||
user_frequency = frappe.db.get_value("User", user, "document_follow_frequency")
|
||||
message = []
|
||||
valid_document_follows = []
|
||||
if user_frequency == frequency:
|
||||
for d in grouped_by_user[user]:
|
||||
content = get_message(d.ref_docname, d.ref_doctype, frequency)
|
||||
if content:
|
||||
message = message + content
|
||||
valid_document_follows.append({
|
||||
"reference_docname": d.ref_docname,
|
||||
"reference_doctype": d.ref_doctype,
|
||||
"reference_url": get_url_to_form(d.ref_doctype, d.ref_docname)
|
||||
})
|
||||
|
||||
if message:
|
||||
send_email_alert(user, valid_document_follows, message)
|
||||
|
||||
|
||||
def get_version(doctype, doc_name, frequency):
|
||||
timeline = []
|
||||
filters = get_filters("docname", doc_name, frequency)
|
||||
version = frappe.get_all("Version",
|
||||
filters=filters,
|
||||
fields=["ref_doctype", "data", "modified", "modified", "modified_by"]
|
||||
)
|
||||
if version:
|
||||
for v in version:
|
||||
change = frappe.parse_json(v.data)
|
||||
time = frappe.utils.format_datetime(v.modified, "hh:mm a")
|
||||
timeline_items = []
|
||||
if change.changed:
|
||||
timeline_items = get_field_changed(change.changed, time, doctype, doc_name, v)
|
||||
if change.row_changed:
|
||||
timeline_items = get_row_changed(change.row_changed, time, doctype, doc_name, v)
|
||||
if change.added:
|
||||
timeline_items = get_added_row(change.added, time, doctype, doc_name, v)
|
||||
|
||||
timeline = timeline + timeline_items
|
||||
|
||||
return timeline
|
||||
|
||||
def get_comments(doctype, doc_name, frequency):
|
||||
timeline = []
|
||||
filters = get_filters("reference_name", doc_name, frequency)
|
||||
comments = frappe.get_all("Comment",
|
||||
filters=filters,
|
||||
fields=["content", "modified", "modified_by", "comment_type"]
|
||||
)
|
||||
for comment in comments:
|
||||
if comment.comment_type == "Like":
|
||||
by = ''' By : <b>{0}<b>'''.format(comment.modified_by)
|
||||
elif comment.comment_type == "Comment":
|
||||
by = '''Commented by : <b>{0}<b>'''.format(comment.modified_by)
|
||||
else:
|
||||
by = ''
|
||||
|
||||
time = frappe.utils.format_datetime(comment.modified, "hh:mm a")
|
||||
timeline.append({
|
||||
"time": comment.modified,
|
||||
"data": {
|
||||
"time": time,
|
||||
"comment": frappe.utils.html2text(str(comment.content)),
|
||||
"by": by
|
||||
},
|
||||
"doctype": doctype,
|
||||
"doc_name": doc_name,
|
||||
"type": "comment"
|
||||
})
|
||||
return timeline
|
||||
|
||||
def is_document_followed(doctype, doc_name, user):
|
||||
docs = frappe.get_all(
|
||||
"Document Follow",
|
||||
filters={
|
||||
"ref_doctype": doctype,
|
||||
"ref_docname": doc_name,
|
||||
"user": user
|
||||
},
|
||||
limit=1
|
||||
)
|
||||
return len(docs)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_follow_users(doctype, doc_name):
|
||||
return frappe.get_all(
|
||||
"Document Follow",
|
||||
filters={
|
||||
"ref_doctype": doctype,
|
||||
"ref_docname":doc_name
|
||||
},
|
||||
fields=["user"]
|
||||
)
|
||||
|
||||
def get_row_changed(row_changed, time, doctype, doc_name, v):
|
||||
items = []
|
||||
for d in row_changed:
|
||||
d[2] = d[2] if d[2] else ' '
|
||||
d[0] = d[0] if d[0] else ' '
|
||||
d[3][0][1] = d[3][0][1] if d[3][0][1] else ' '
|
||||
items.append({
|
||||
"time": v.modified,
|
||||
"data": {
|
||||
"time": time,
|
||||
"table_field": d[0],
|
||||
"row": str(d[1]),
|
||||
"field": d[3][0][0],
|
||||
"from": frappe.utils.html2text(str(d[3][0][1])),
|
||||
"to": frappe.utils.html2text(str(d[3][0][2]))
|
||||
},
|
||||
"doctype": doctype,
|
||||
"doc_name": doc_name,
|
||||
"type": "row changed",
|
||||
"by": v.modified_by
|
||||
})
|
||||
return items
|
||||
|
||||
def get_added_row(added, time, doctype, doc_name, v):
|
||||
items = []
|
||||
for d in added:
|
||||
items.append({
|
||||
"time": v.modified,
|
||||
"data": {
|
||||
"to": d[0],
|
||||
"time": time
|
||||
},
|
||||
"doctype": doctype,
|
||||
"doc_name": doc_name,
|
||||
"type": "row added",
|
||||
"by": v.modified_by
|
||||
})
|
||||
return items
|
||||
|
||||
def get_field_changed(changed, time, doctype, doc_name, v):
|
||||
items = []
|
||||
for d in changed:
|
||||
d[1] = d[1] if d[1] else ' '
|
||||
d[2] = d[2] if d[2] else ' '
|
||||
d[0] = d[0] if d[0] else ' '
|
||||
items.append({
|
||||
"time": v.modified,
|
||||
"data": {
|
||||
"time": time,
|
||||
"field": d[0],
|
||||
"from": frappe.utils.html2text(str(d[1])),
|
||||
"to": frappe.utils.html2text(str(d[2]))
|
||||
},
|
||||
"doctype": doctype,
|
||||
"doc_name": doc_name,
|
||||
"type": "field changed",
|
||||
"by": v.modified_by
|
||||
})
|
||||
return items
|
||||
|
||||
def send_hourly_updates():
|
||||
send_document_follow_mails("Hourly")
|
||||
|
||||
def send_daily_updates():
|
||||
send_document_follow_mails("Daily")
|
||||
|
||||
def send_weekly_updates():
|
||||
send_document_follow_mails("Weekly")
|
||||
|
||||
def get_filters(search_by, name, frequency):
|
||||
filters = []
|
||||
|
||||
if frequency == "Weekly":
|
||||
filters = [
|
||||
[search_by, "=", name],
|
||||
["modified", ">", frappe.utils.add_days(frappe.utils.nowdate(),-7)],
|
||||
["modified", "<", frappe.utils.nowdate()]
|
||||
]
|
||||
elif frequency == "Daily":
|
||||
filters = [
|
||||
[search_by, "=", name],
|
||||
["modified", ">", frappe.utils.add_days(frappe.utils.nowdate(),-1)],
|
||||
["modified", "<", frappe.utils.nowdate()]
|
||||
]
|
||||
elif frequency == "Hourly":
|
||||
filters = [
|
||||
[search_by, "=", name],
|
||||
["modified", ">", frappe.utils.add_to_date(frappe.utils.now_datetime(), 0, 0, 0, -1)],
|
||||
["modified", "<", frappe.utils.now_datetime()]
|
||||
]
|
||||
|
||||
return filters
|
||||
|
|
@ -9,6 +9,7 @@ import frappe.defaults
|
|||
import frappe.desk.form.meta
|
||||
from frappe.model.utils.user_settings import get_user_settings
|
||||
from frappe.permissions import get_doc_permissions
|
||||
from frappe.desk.form.document_follow import is_document_followed
|
||||
from frappe import _
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -90,7 +91,6 @@ def get_docinfo(doc=None, doctype=None, name=None):
|
|||
doc = frappe.get_doc(doctype, name)
|
||||
if not doc.has_permission("read"):
|
||||
raise frappe.PermissionError
|
||||
|
||||
frappe.response["docinfo"] = {
|
||||
"attachments": get_attachments(doc.doctype, doc.name),
|
||||
"communications": _get_communications(doc.doctype, doc.name),
|
||||
|
|
@ -101,7 +101,9 @@ def get_docinfo(doc=None, doctype=None, name=None):
|
|||
"permissions": get_doc_permissions(doc),
|
||||
"shared": frappe.share.get_users(doc.doctype, doc.name),
|
||||
"rating": get_feedback_rating(doc.doctype, doc.name),
|
||||
"views": get_view_logs(doc.doctype, doc.name)
|
||||
"views": get_view_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")
|
||||
}
|
||||
|
||||
def get_attachments(dt, dn):
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import frappe, json
|
|||
import frappe.desk.form.meta
|
||||
import frappe.desk.form.load
|
||||
from frappe.utils.html_utils import clean_email_html
|
||||
from frappe.desk.form.document_follow import follow_document
|
||||
|
||||
from frappe import _
|
||||
from six import string_types
|
||||
|
|
@ -68,6 +69,7 @@ def add_comment(reference_doctype, reference_name, content, comment_email):
|
|||
comment_type = 'Comment'
|
||||
)).insert(ignore_permissions = True)
|
||||
|
||||
follow_document(doc.reference_doctype, doc.reference_name, frappe.session.user)
|
||||
return doc.as_dict()
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from __future__ import unicode_literals
|
|||
import frappe, json
|
||||
from frappe.database.schema import add_column
|
||||
from frappe import _
|
||||
from frappe.desk.form.document_follow import follow_document
|
||||
from frappe.utils import get_link_to_form
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -46,7 +47,7 @@ def _toggle_like(doctype, name, add, user=None):
|
|||
if user not in liked_by:
|
||||
liked_by.append(user)
|
||||
add_comment(doctype, name)
|
||||
|
||||
follow_document(doctype, name, user)
|
||||
else:
|
||||
if user in liked_by:
|
||||
liked_by.remove(user)
|
||||
|
|
|
|||
0
frappe/email/doctype/document_follow/__init__.py
Normal file
0
frappe/email/doctype/document_follow/__init__.py
Normal file
6
frappe/email/doctype/document_follow/document_follow.js
Normal file
6
frappe/email/doctype/document_follow/document_follow.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (c) 2019, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Document Follow', {
|
||||
|
||||
});
|
||||
181
frappe/email/doctype/document_follow/document_follow.json
Normal file
181
frappe/email/doctype/document_follow/document_follow.json
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2019-01-09 16:39:23.746535",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "ref_doctype",
|
||||
"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": "Doctype",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "DocType",
|
||||
"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,
|
||||
"fieldname": "ref_docname",
|
||||
"fieldtype": "Dynamic 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 Name",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "ref_doctype",
|
||||
"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,
|
||||
"fieldname": "user",
|
||||
"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": "User",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "User",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 1,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-02-26 15:43:44.330348",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Document Follow",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"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": "All",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 1,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
}
|
||||
10
frappe/email/doctype/document_follow/document_follow.py
Normal file
10
frappe/email/doctype/document_follow/document_follow.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from frappe.model.document import Document
|
||||
|
||||
class DocumentFollow(Document):
|
||||
pass
|
||||
|
||||
23
frappe/email/doctype/document_follow/test_document_follow.js
Normal file
23
frappe/email/doctype/document_follow/test_document_follow.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/* eslint-disable */
|
||||
// rename this file from _test_[name] to test_[name] to activate
|
||||
// and remove above this line
|
||||
|
||||
QUnit.test("test: Document Follow", function (assert) {
|
||||
let done = assert.async();
|
||||
|
||||
// number of asserts
|
||||
assert.expect(1);
|
||||
|
||||
frappe.run_serially([
|
||||
// insert a new Document Follow
|
||||
() => frappe.tests.make('Document Follow', [
|
||||
// values to be set
|
||||
{key: 'value'}
|
||||
]),
|
||||
() => {
|
||||
assert.equal(cur_frm.doc.key, 'value');
|
||||
},
|
||||
() => done()
|
||||
]);
|
||||
|
||||
});
|
||||
56
frappe/email/doctype/document_follow/test_document_follow.py
Normal file
56
frappe/email/doctype/document_follow/test_document_follow.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
import unittest
|
||||
import frappe.desk.form.document_follow as document_follow
|
||||
|
||||
class TestDocumentFollow(unittest.TestCase):
|
||||
|
||||
def test_add_subscription_and_send_mail(self):
|
||||
user = get_user()
|
||||
event_doc = get_event()
|
||||
|
||||
event_doc.description = "This is a test description for sending mail"
|
||||
event_doc.save()
|
||||
|
||||
doc = document_follow.follow_document("Event", event_doc.name , user.name, force=True)
|
||||
self.assertEquals(doc.user, user.name)
|
||||
|
||||
document_follow.send_hourly_updates()
|
||||
|
||||
email_queue_entry_name = frappe.get_all("Email Queue", limit=1)[0].name
|
||||
email_queue_entry_doc = frappe.get_doc("Email Queue", email_queue_entry_name)
|
||||
|
||||
self.assertEquals((email_queue_entry_doc.recipients[0].recipient), user.name)
|
||||
|
||||
self.assertIn(event_doc.doctype, email_queue_entry_doc.message)
|
||||
self.assertIn(event_doc.name, email_queue_entry_doc.message)
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def get_event():
|
||||
doc = frappe.get_doc({
|
||||
'doctype': 'Event',
|
||||
'subject': "_Test_Doc_Follow",
|
||||
'doc.starts_on': frappe.utils.now(),
|
||||
'doc.ends_on': frappe.utils.add_days(frappe.utils.now(),5),
|
||||
'doc.description': "Hello"
|
||||
})
|
||||
doc.insert()
|
||||
return doc
|
||||
|
||||
def get_user():
|
||||
doc = frappe.new_doc("User")
|
||||
doc.email = "test@docsub.com"
|
||||
doc.first_name = "Test"
|
||||
doc.last_name = "User"
|
||||
doc.send_welcome_email = 0
|
||||
doc.document_follow_notify = 1
|
||||
doc.document_follow_frequency = "Hourly"
|
||||
doc.insert()
|
||||
return doc
|
||||
|
|
@ -158,6 +158,7 @@ scheduler_events = {
|
|||
"frappe.limits.update_space_usage",
|
||||
"frappe.desk.doctype.auto_repeat.auto_repeat.make_auto_repeat_entry",
|
||||
"frappe.deferred_insert.save_to_db"
|
||||
"frappe.desk.form.document_follow.send_hourly_updates"
|
||||
],
|
||||
"daily": [
|
||||
"frappe.email.queue.clear_outbox",
|
||||
|
|
@ -172,6 +173,7 @@ scheduler_events = {
|
|||
"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.desk.form.document_follow.send_daily_updates"
|
||||
],
|
||||
"daily_long": [
|
||||
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily",
|
||||
|
|
@ -182,6 +184,7 @@ scheduler_events = {
|
|||
"frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_weekly",
|
||||
"frappe.utils.change_log.check_for_update",
|
||||
"frappe.desk.doctype.route_history.route_history.flush_old_route_records"
|
||||
"frappe.desk.form.document_follow.send_weekly_updates"
|
||||
],
|
||||
"monthly": [
|
||||
"frappe.email.doctype.auto_email_report.auto_email_report.send_monthly"
|
||||
|
|
|
|||
|
|
@ -192,7 +192,6 @@ class DatabaseQuery(object):
|
|||
field which may leads to sql injection.
|
||||
example :
|
||||
field = "`DocType`.`issingle`, version()"
|
||||
|
||||
As field contains `,` and mysql function `version()`, with the help of regex
|
||||
the system will filter out this field.
|
||||
'''
|
||||
|
|
@ -326,7 +325,6 @@ class DatabaseQuery(object):
|
|||
|
||||
def prepare_filter_condition(self, f):
|
||||
"""Returns a filter condition in the format:
|
||||
|
||||
ifnull(`tabDocType`.`fieldname`, fallback) operator "value"
|
||||
"""
|
||||
|
||||
|
|
@ -810,4 +808,4 @@ def get_between_date_filter(value, df=None):
|
|||
frappe.db.format_date(from_date),
|
||||
frappe.db.format_date(to_date))
|
||||
|
||||
return data
|
||||
return data
|
||||
|
|
@ -195,7 +195,7 @@ def check_if_doc_is_linked(doc, method="Delete"):
|
|||
for item in frappe.db.get_values(link_dt, {link_field:doc.name},
|
||||
["name", "parent", "parenttype", "docstatus"], as_dict=True):
|
||||
linked_doctype = item.parenttype if item.parent else link_dt
|
||||
if linked_doctype in ("Communication", "ToDo", "DocShare", "Email Unsubscribe", 'File', 'Version', "Activity Log", 'Comment'):
|
||||
if linked_doctype in ("Communication", "ToDo", "DocShare", "Email Unsubscribe", 'File', 'Version', "Activity Log", "Document Follow"):
|
||||
# don't check for communication and todo!
|
||||
continue
|
||||
|
||||
|
|
@ -220,7 +220,7 @@ def check_if_doc_is_linked(doc, method="Delete"):
|
|||
def check_if_doc_is_dynamically_linked(doc, method="Delete"):
|
||||
'''Raise `frappe.LinkExistsError` if the document is dynamically linked'''
|
||||
for df in get_dynamic_link_map().get(doc.doctype, []):
|
||||
if df.parent in ("Communication", "ToDo", "DocShare", "Email Unsubscribe", "Activity Log", 'File', 'Version', 'View Log', 'Comment'):
|
||||
if df.parent in ("Communication", "ToDo", "DocShare", "Email Unsubscribe", "Activity Log", 'File', 'Version', 'View Log', "Document Follow"):
|
||||
# don't check for communication and todo!
|
||||
continue
|
||||
|
||||
|
|
@ -272,6 +272,7 @@ def delete_dynamic_links(doctype, name):
|
|||
delete_references('Version', doctype, name, 'ref_doctype', 'docname')
|
||||
delete_references('Comment', doctype, name)
|
||||
delete_references('View Log', doctype, name)
|
||||
delete_references('Document Follow', doctype, name, 'ref_doctype', 'ref_docname')
|
||||
|
||||
# unlink communications
|
||||
clear_references('Communication', doctype, name)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from frappe.model import optional_fields, table_fields
|
|||
from frappe.model.workflow import validate_workflow
|
||||
from frappe.utils.global_search import update_global_search
|
||||
from frappe.integrations.doctype.webhook import run_webhooks
|
||||
from frappe.desk.form.document_follow import follow_document
|
||||
|
||||
# once_only validation
|
||||
# methods
|
||||
|
|
@ -1014,6 +1015,7 @@ class Document(BaseDocument):
|
|||
version = frappe.new_doc('Version')
|
||||
if version.set_diff(self._doc_before_save, self):
|
||||
version.insert(ignore_permissions=True)
|
||||
follow_document(self.doctype, self.name, frappe.session.user)
|
||||
|
||||
@staticmethod
|
||||
def whitelist(f):
|
||||
|
|
|
|||
|
|
@ -231,6 +231,7 @@ frappe.patches.v11_0.set_missing_creation_and_modified_value_for_user_permission
|
|||
frappe.patches.v11_0.remove_doctype_user_permissions_for_page_and_report
|
||||
frappe.patches.v11_0.set_default_letter_head_source
|
||||
frappe.patches.v12_0.set_primary_key_in_series
|
||||
execute:frappe.reload_doc('email', 'doctype', 'document_follow')
|
||||
execute:frappe.delete_doc("Page", "modules", ignore_missing=True)
|
||||
frappe.patches.v11_0.set_default_letter_head_source
|
||||
frappe.patches.v12_0.setup_comments_from_communications
|
||||
|
|
|
|||
|
|
@ -268,6 +268,7 @@
|
|||
],
|
||||
"js/form.min.js": [
|
||||
"public/js/frappe/form/templates/print_layout.html",
|
||||
"public/js/frappe/form/document_follow.js",
|
||||
"public/js/frappe/form/templates/users_in_sidebar.html",
|
||||
"public/js/frappe/form/templates/set_sharing.html",
|
||||
"public/js/frappe/form/templates/form_sidebar.html",
|
||||
|
|
|
|||
|
|
@ -164,6 +164,10 @@ hr {
|
|||
margin: 8px 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
.list-unstyled {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
/* auto email report */
|
||||
.report-title {
|
||||
margin-bottom: 20px;
|
||||
|
|
|
|||
144
frappe/public/js/frappe/form/document_follow.js
Normal file
144
frappe/public/js/frappe/form/document_follow.js
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// MIT License. See license.txt
|
||||
|
||||
frappe.provide('frappe.ui.form');
|
||||
|
||||
frappe.ui.form.DocumentFollow = class DocumentFollow {
|
||||
constructor(opts) {
|
||||
$.extend(this, opts);
|
||||
this.follow_document_link = this.parent.find('.follow-document-link');
|
||||
this.unfollow_document_link = this.parent.find('.unfollow-document-link');
|
||||
this.follow_span = this.parent.find('.anchor-document-follow > span');
|
||||
this.followed_by = this.parent.find('.followed-by');
|
||||
this.followed_by_label = this.parent.find('.followed-by-label');
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.set_followers();
|
||||
this.render_sidebar();
|
||||
}
|
||||
|
||||
render_sidebar() {
|
||||
const docinfo = this.frm.get_docinfo();
|
||||
const document_follow_enabled = docinfo && docinfo.document_follow_enabled;
|
||||
const document_can_be_followed = frappe.get_meta(this.frm.doctype).track_changes;
|
||||
if (frappe.session.user === 'Administrator'
|
||||
|| !document_follow_enabled
|
||||
|| !document_can_be_followed
|
||||
) {
|
||||
this.hide_follow_section();
|
||||
return;
|
||||
}
|
||||
this.bind_events();
|
||||
|
||||
const is_followed = docinfo && docinfo.is_document_followed;
|
||||
|
||||
if(is_followed > 0) {
|
||||
this.unfollow_document_link.removeClass('hidden');
|
||||
this.follow_document_link.addClass('hidden');
|
||||
} else {
|
||||
this.followed_by_label.addClass('hidden');
|
||||
this.followed_by.addClass('hidden');
|
||||
this.unfollow_document_link.addClass('hidden');
|
||||
this.follow_document_link.removeClass('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
bind_events() {
|
||||
this.follow_document_link.on('click', () => {
|
||||
this.follow_document_link.addClass('text-muted disable-click');
|
||||
frappe.call({
|
||||
method: 'frappe.desk.form.document_follow.follow_document',
|
||||
args: {
|
||||
'doctype': this.frm.doctype,
|
||||
'doc_name': this.frm.doc.name,
|
||||
'user': frappe.session.user,
|
||||
'force': true
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
this.follow_action();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.unfollow_document_link.on('click', () => {
|
||||
this.unfollow_document_link.addClass('text-muted disable-click');
|
||||
frappe.call({
|
||||
method: 'frappe.desk.form.document_follow.unfollow_document',
|
||||
args: {
|
||||
'doctype': this.frm.doctype,
|
||||
'doc_name': this.frm.doc.name,
|
||||
'user': frappe.session.user
|
||||
},
|
||||
callback: (r) => {
|
||||
if(r.message) {
|
||||
this.unfollow_action();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
hide_follow_section() {
|
||||
this.parent.hide();
|
||||
}
|
||||
|
||||
set_followers() {
|
||||
this.followed_by.removeClass('hidden');
|
||||
this.followed_by_label.removeClass('hidden');
|
||||
this.followed_by.empty();
|
||||
this.get_followed_user().then(user => {
|
||||
$(user).appendTo(this.followed_by);
|
||||
});
|
||||
}
|
||||
|
||||
get_followed_user() {
|
||||
var html = '';
|
||||
return new Promise(resolve => {
|
||||
frappe.call({
|
||||
method: 'frappe.desk.form.document_follow.get_follow_users',
|
||||
args: {
|
||||
'doctype': this.frm.doctype,
|
||||
'doc_name': this.frm.doc.name,
|
||||
},
|
||||
}).then(r => {
|
||||
this.count_others = 0;
|
||||
for (var d in r.message) {
|
||||
this.count_others++;
|
||||
if(this.count_others < 4){
|
||||
html += frappe.avatar(r.message[d].user, 'avatar-small');
|
||||
}
|
||||
if(this.count_others === 0){
|
||||
this.followed_by.addClass('hidden');
|
||||
}
|
||||
}
|
||||
resolve(html);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
follow_action() {
|
||||
frappe.show_alert({
|
||||
message: __('You are now following this document. You will receive daily updates via email. You can change this in User Settings.'),
|
||||
indicator: 'orange'
|
||||
});
|
||||
this.follow_document_link.removeClass('text-muted disable-click');
|
||||
this.follow_document_link.addClass('hidden');
|
||||
this.unfollow_document_link.removeClass('hidden');
|
||||
this.set_followers();
|
||||
}
|
||||
|
||||
unfollow_action() {
|
||||
frappe.show_alert({
|
||||
message: __('You unfollowed this document'),
|
||||
indicator: 'red'
|
||||
});
|
||||
this.unfollow_document_link.removeClass('text-muted disable-click');
|
||||
this.unfollow_document_link.addClass('hidden');
|
||||
this.follow_document_link.removeClass('hidden');
|
||||
this.followed_by.addClass('hidden');
|
||||
this.followed_by_label.addClass('hidden');
|
||||
}
|
||||
};
|
||||
|
|
@ -16,13 +16,13 @@ frappe.ui.form.Sidebar = Class.extend({
|
|||
this.user_actions = this.sidebar.find(".user-actions");
|
||||
this.image_section = this.sidebar.find(".sidebar-image-section");
|
||||
this.image_wrapper = this.image_section.find('.sidebar-image-wrapper');
|
||||
|
||||
this.make_assignments();
|
||||
this.make_attachments();
|
||||
this.make_shared();
|
||||
this.make_viewers();
|
||||
this.make_tags();
|
||||
this.make_like();
|
||||
this.make_follow();
|
||||
|
||||
this.bind_events();
|
||||
frappe.ui.form.setup_user_image_event(this.frm);
|
||||
|
|
@ -54,6 +54,7 @@ frappe.ui.form.Sidebar = Class.extend({
|
|||
this.frm.assign_to.refresh();
|
||||
this.frm.attachments.refresh();
|
||||
this.frm.shared.refresh();
|
||||
this.frm.follow.refresh();
|
||||
this.frm.viewers.refresh();
|
||||
this.frm.tags && this.frm.tags.refresh(this.frm.doc._user_tags);
|
||||
this.sidebar.find(".modified-by").html(__("{0} edited this {1}",
|
||||
|
|
@ -131,7 +132,12 @@ frappe.ui.form.Sidebar = Class.extend({
|
|||
this.like_count = this.sidebar.find(".liked-by .likes-count");
|
||||
frappe.ui.setup_like_popover(this.sidebar.find(".liked-by-parent"), ".liked-by");
|
||||
},
|
||||
|
||||
make_follow: function(){
|
||||
this.frm.follow = new frappe.ui.form.DocumentFollow({
|
||||
frm: this.frm,
|
||||
parent: this.sidebar.find(".followed-by-section")
|
||||
});
|
||||
},
|
||||
refresh_like: function() {
|
||||
if (!this.like_icon) {
|
||||
return;
|
||||
|
|
@ -149,7 +155,6 @@ frappe.ui.form.Sidebar = Class.extend({
|
|||
|
||||
refresh_image: function() {
|
||||
},
|
||||
|
||||
setup_ratings: function() {
|
||||
var _ratings = this.frm.get_docinfo().rating || 0;
|
||||
|
||||
|
|
@ -158,5 +163,5 @@ frappe.ui.form.Sidebar = Class.extend({
|
|||
var rating_icons = frappe.render_template("rating_icons", {rating: _ratings, show_label: false});
|
||||
this.ratings.find(".rating-icons").html(rating_icons);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -68,13 +68,27 @@
|
|||
<li class="h6 viewers-label">{%= __("Currently Viewing") %}</li>
|
||||
<li class="form-viewers"></li>
|
||||
</ul>
|
||||
<ul class="list-unstyled sidebar-menu text-muted">
|
||||
<ul class="list-unstyled sidebar-menu">
|
||||
<li class="liked-by-parent">
|
||||
<span class="liked-by">
|
||||
<i class="octicon octicon-heart like-action text-extra-muted fa-fw"></i>
|
||||
<span class="likes-count"></span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="list-unstyled sidebar-menu followed-by-section">
|
||||
<li class="h6 followed-by-label text-medium hidden">{%= __("Followed by") %}</li>
|
||||
<li class="followed-by"></li>
|
||||
<li class="document-follow">
|
||||
<a class="strong badge-hover follow-document-link hidden">
|
||||
{%= __("Follow") %}
|
||||
</a>
|
||||
<a class="strong badge-hover unfollow-document-link hidden">
|
||||
{%= __("Unfollow") %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="list-unstyled sidebar-menu text-muted">
|
||||
<li class="modified-by"></li>
|
||||
<li class="created-by"></li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -201,6 +201,11 @@ hr {
|
|||
max-width: 100%;
|
||||
}
|
||||
|
||||
.list-unstyled {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* auto email report */
|
||||
.report-title {
|
||||
margin-bottom: 20px;
|
||||
|
|
|
|||
|
|
@ -943,3 +943,7 @@ body[data-route^="Form/Communication"] textarea[data-fieldname="subject"] {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.followed-by-label{
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.desk.form.document_follow import follow_document
|
||||
from frappe.utils import cint
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -41,6 +42,8 @@ def add(doctype, name, user=None, read=1, write=0, share=0, everyone=0, flags=No
|
|||
doc.save(ignore_permissions=True)
|
||||
notify_assignment(user, doctype, name, description=None, notify=notify)
|
||||
|
||||
follow_document(doctype, name, user)
|
||||
|
||||
return doc
|
||||
|
||||
def remove(doctype, name, user, flags=None):
|
||||
|
|
|
|||
88
frappe/templates/emails/document_follow.html
Normal file
88
frappe/templates/emails/document_follow.html
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<h3>Document Follow Notification</h3>
|
||||
</tr>
|
||||
</table>
|
||||
{% for doc in docinfo%}
|
||||
<table class="panel-header" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr height="10"></tr>
|
||||
<tr>
|
||||
<td width="15"></td>
|
||||
<td>
|
||||
<div class="text-medium text-muted">
|
||||
<span><a href="{{doc.reference_url}}">{{ doc.reference_doctype }}: {{doc.reference_docname }}</a></span>
|
||||
</div>
|
||||
</td>
|
||||
<td width="15"></td>
|
||||
</tr>
|
||||
<tr height="10"></tr>
|
||||
</table>
|
||||
<table class="panel-body" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr height="10"></tr>
|
||||
<tr>
|
||||
<td width="15"></td>
|
||||
<td>
|
||||
<div>
|
||||
<ul class="list-unstyled" style="line-height: 1.7">
|
||||
{% for data in timeline %}
|
||||
{% if (data.doctype == doc.reference_doctype and data.doc_name == doc.reference_docname) %}
|
||||
{% if data.type == "comment" %}
|
||||
<li>
|
||||
<span style ='color:#8d99a6!important'>
|
||||
{{data.data.time}}:
|
||||
</span>
|
||||
<b>"{{data.data.comment}}"</b>
|
||||
{{data.data.by}}
|
||||
</li>
|
||||
{% elif data.type == "row added" %}
|
||||
<li>
|
||||
<span style ='color:#8d99a6!important'>
|
||||
{{data.data.time}}:
|
||||
</span>
|
||||
Row Added to Table Field
|
||||
<b>{{data.data.to}}</b>
|
||||
By:
|
||||
<b>{{data.by}}</b>
|
||||
</li>
|
||||
{% elif data.type == "field changed" %}
|
||||
<li>
|
||||
<span style ='color:#8d99a6!important'>
|
||||
{{data.data.time}}:
|
||||
</span>Field:
|
||||
<b>"{{data.data.field}}"</b>
|
||||
changed from
|
||||
<b>"{{data.data.from}}"</b>
|
||||
to
|
||||
<b>"{{data.data.to}}"</b>
|
||||
By:
|
||||
<b>{{data.by}}</b>
|
||||
</li>
|
||||
{% elif data.type == "row changed" %}
|
||||
<li>
|
||||
<span style ='color:#8d99a6!important'>
|
||||
{{data.data.time}}:
|
||||
</span>
|
||||
Table Field:
|
||||
<b>"{{data.data.table_field}}"</b>
|
||||
Row# {{data.data.row}} Field:
|
||||
<b>"{{data.data.field}}"</b>
|
||||
changed from
|
||||
<b>"{{data.data.from}}" </b>
|
||||
to <b>"{{data.data.to}}"</b>
|
||||
By:
|
||||
<b>{{data.by}}</b>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
<td width="15"></td>
|
||||
</tr>
|
||||
<tr height="10"></tr>
|
||||
</table>
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr height="20"></tr>
|
||||
</table>
|
||||
{% endfor %}
|
||||
|
|
@ -339,12 +339,12 @@ class TestReportview(unittest.TestCase):
|
|||
|
||||
|
||||
def test_is_set_is_not_set(self):
|
||||
res = DatabaseQuery("DocType").execute(filters={"autoname": ["is", "not set"]})
|
||||
res = DatabaseQuery('DocType').execute(filters={'autoname': ['is', 'not set']})
|
||||
self.assertTrue({'name': 'Integration Request'} in res)
|
||||
self.assertTrue({'name': 'User'} in res)
|
||||
self.assertFalse({'name': 'Blogger'} in res)
|
||||
|
||||
res = DatabaseQuery("DocType").execute(filters={"autoname": ["is", "set"]})
|
||||
res = DatabaseQuery('DocType').execute(filters={'autoname': ['is', 'set']})
|
||||
self.assertTrue({'name': 'DocField'} in res)
|
||||
self.assertTrue({'name': 'Prepared Report'} in res)
|
||||
self.assertFalse({'name': 'Property Setter'} in res)
|
||||
|
|
|
|||
|
|
@ -564,7 +564,9 @@ def parse_json(val):
|
|||
Parses json if string else return
|
||||
"""
|
||||
if isinstance(val, string_types):
|
||||
return json.loads(val)
|
||||
val = json.loads(val)
|
||||
if isinstance(val, dict):
|
||||
val = frappe._dict(val)
|
||||
return val
|
||||
|
||||
def cast_fieldtype(fieldtype, value):
|
||||
|
|
|
|||
|
|
@ -326,7 +326,6 @@ def ceil(s):
|
|||
|
||||
def cstr(s, encoding='utf-8'):
|
||||
return frappe.as_unicode(s, encoding)
|
||||
|
||||
def rounded(num, precision=0):
|
||||
"""round method for round halfs to nearest even algorithm aka banker's rounding - compatible with python3"""
|
||||
precision = cint(precision)
|
||||
|
|
|
|||
|
|
@ -205,9 +205,9 @@ def get_frame_locals():
|
|||
frames = []
|
||||
if traceback:
|
||||
frames = inspect.getinnerframes(traceback, context=0)
|
||||
_locals = ['Locals (most recent call last):']
|
||||
for frame, filename, lineno, function, __, __ in frames:
|
||||
if '/apps/' in filename:
|
||||
_locals.append('File "{}", line {}, in {}\n{}'.format(filename, lineno, function, json.dumps(frame.f_locals, default=str, indent=4)))
|
||||
_locals = ['Locals (most recent call last):']
|
||||
for frame, filename, lineno, function, __, __ in frames:
|
||||
if '/apps/' in filename:
|
||||
_locals.append('File "{}", line {}, in {}\n{}'.format(filename, lineno, function, json.dumps(frame.f_locals, default=str, indent=4)))
|
||||
|
||||
return '\n'.join(_locals)
|
||||
return '\n'.join(_locals)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue