From af69d99f88a813d806acb0d5755b88a45d67d596 Mon Sep 17 00:00:00 2001 From: Sachin Mane Date: Mon, 8 Apr 2019 15:49:17 +0530 Subject: [PATCH 01/60] add frappe cmd and doctype in http header for better analytics and load balancing --- frappe/public/js/frappe/request.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/request.js b/frappe/public/js/frappe/request.js index cd102117d7..fbfdb65532 100644 --- a/frappe/public/js/frappe/request.js +++ b/frappe/public/js/frappe/request.js @@ -196,11 +196,16 @@ frappe.request.call = function(opts) { async: opts.async, headers: Object.assign({ "X-Frappe-CSRF-Token": frappe.csrf_token, - "Accept": "application/json" + "Accept": "application/json", + "X-Frappe-CMD": (opts.args && opts.args.cmd || '') || '' }, opts.headers), cache: false }; + if (opts.args && opts.args.doctype) { + ajax_args.headers["X-Frappe-Doctype"] = opts.args.doctype; + } + frappe.last_request = ajax_args.data; return $.ajax(ajax_args) From 0ea9a01704c6e80bb4f73f8032d838f0c18132d0 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 16 Apr 2019 16:00:07 +0530 Subject: [PATCH 02/60] Update frappe/public/js/frappe/request.js Co-Authored-By: sachinhub --- frappe/public/js/frappe/request.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/request.js b/frappe/public/js/frappe/request.js index fbfdb65532..ee21f121bc 100644 --- a/frappe/public/js/frappe/request.js +++ b/frappe/public/js/frappe/request.js @@ -197,7 +197,7 @@ frappe.request.call = function(opts) { headers: Object.assign({ "X-Frappe-CSRF-Token": frappe.csrf_token, "Accept": "application/json", - "X-Frappe-CMD": (opts.args && opts.args.cmd || '') || '' + "X-Frappe-CMD": opts.get('args', {}).get('cmd', '') }, opts.headers), cache: false }; From 5e284db2060d93a3e3351207fa8cd91f0c51209d Mon Sep 17 00:00:00 2001 From: Saurabh Date: Fri, 3 May 2019 18:17:04 +0530 Subject: [PATCH 03/60] fix: naming and provision to use same db credentails for secondary --- frappe/__init__.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 75684d431e..1525c92cd5 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -187,14 +187,19 @@ def connect(site=None, db_name=None): local.db = get_db(user=db_name or local.conf.db_name) set_user("Administrator") -def connect_read_only(): +def connect_secondary(): from frappe.database import get_db + user = local.conf.db_name + password = local.conf.db_password - local.read_only_db = get_db(host=local.conf.slave_host, user=local.conf.slave_db_name, - password=local.conf.slave_db_password) + if local.conf.different_credentials_for_secondary: + user = local.conf.secondary_db_name + password = local.conf.secondary_db_password + + local.read_only_db = get_db(host=local.conf.secondary_host, user=user, password=password) # swap db connections - local.master_db = local.db + local.primary_db = local.db local.db = local.read_only_db def get_site_config(sites_path=None, site_path=None): @@ -495,16 +500,17 @@ def whitelist(allow_guest=False, xss_safe=False): def read_only(): def innfn(fn): def wrapper_fn(*args, **kwargs): - if conf.use_slave_for_read_only: - connect_read_only() + if conf.read_from_secondary: + connect_secondary() + try: retval = fn(*args, **get_newargs(fn, kwargs)) except: raise finally: - if local and hasattr(local, 'master_db'): + if local and hasattr(local, 'primary_db'): local.db.close() - local.db = local.master_db + local.db = local.primary_db return retval return wrapper_fn From ca01c75ea410d2be2b1f85ed2cd6f689423b3250 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Fri, 10 May 2019 10:54:17 +0530 Subject: [PATCH 04/60] fix: naming --- frappe/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 1525c92cd5..cd662cd6cb 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -187,20 +187,20 @@ def connect(site=None, db_name=None): local.db = get_db(user=db_name or local.conf.db_name) set_user("Administrator") -def connect_secondary(): +def connect_replica(): from frappe.database import get_db user = local.conf.db_name password = local.conf.db_password - if local.conf.different_credentials_for_secondary: - user = local.conf.secondary_db_name - password = local.conf.secondary_db_password + if local.conf.different_credentials_for_replica: + user = local.conf.replica_db_name + password = local.conf.replica_db_password - local.read_only_db = get_db(host=local.conf.secondary_host, user=user, password=password) + local.replica_db = get_db(host=local.conf.replica_host, user=user, password=password) # swap db connections local.primary_db = local.db - local.db = local.read_only_db + local.db = local.replica_db def get_site_config(sites_path=None, site_path=None): """Returns `site_config.json` combined with `sites/common_site_config.json`. @@ -500,8 +500,8 @@ def whitelist(allow_guest=False, xss_safe=False): def read_only(): def innfn(fn): def wrapper_fn(*args, **kwargs): - if conf.read_from_secondary: - connect_secondary() + if conf.read_from_replica: + connect_replica() try: retval = fn(*args, **get_newargs(fn, kwargs)) From 8381e0048a1bb5d61860af072f8a1ff61fc7ee13 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Mon, 13 May 2019 20:39:49 +0530 Subject: [PATCH 05/60] feat: Communication refactor initial bringup --- .../doctype/communication/communication.js | 82 +++++++++++++++++- .../doctype/communication/communication.json | 39 ++++----- .../doctype/communication/communication.py | 83 +++++++++++++++++-- frappe/core/doctype/communication/email.py | 45 ++++++++++ frappe/desk/form/load.py | 59 ++++++++----- .../doctype/email_account/email_account.py | 9 +- 6 files changed, 260 insertions(+), 57 deletions(-) diff --git a/frappe/core/doctype/communication/communication.js b/frappe/core/doctype/communication/communication.js index 8e35c388a5..ecf2b4670d 100644 --- a/frappe/core/doctype/communication/communication.js +++ b/frappe/core/doctype/communication/communication.js @@ -54,7 +54,15 @@ frappe.ui.form.on("Communication", { frm.trigger('show_relink_dialog'); }); - if(frm.doc.communication_type=="Communication" + frm.add_custom_button(__("Add link"), function() { + frm.trigger('show_add_link_dialog'); + }); + + frm.add_custom_button(__("Remove link"), function() { + frm.trigger('show_remove_link_dialog'); + }); + + if(frm.doc.communication_type=="Communication" && frm.doc.communication_medium == "Email" && frm.doc.sent_or_received == "Received") { @@ -90,7 +98,7 @@ frappe.ui.form.on("Communication", { } } - if(frm.doc.communication_type=="Communication" + if(frm.doc.communication_type=="Communication" && frm.doc.communication_medium == "Phone" && frm.doc.sent_or_received == "Received"){ @@ -148,6 +156,74 @@ frappe.ui.form.on("Communication", { d.show(); }, + show_add_link_dialog: function(frm){ + var d = new frappe.ui.Dialog ({ + title: __("Add new link to Communication"), + fields: [{ + "fieldtype": "Link", + "options": "DocType", + "label": __("Document Type"), + "fieldname": "link_doctype", + "reqd": 1 + }, + { + "fieldtype": "Dynamic Link", + "options": "link_doctype", + "label": __("Document Name"), + "fieldname": "link_name", + "reqd": 1 + }], + primary_action: ({ link_doctype, link_name }) => { + d.hide(); + frm.call('add_link', { + link_doctype, + link_name, + autosave: true + }).then(() => frm.refresh()); + }, + primary_action_label: __('Add Link') + }); + d.fields_dict.link_doctype.get_query = function() { + return { + "filters": { + "name": ["!=", "Communication"], + } + }; + }; + d.show(); + }, + + show_remove_link_dialog: function(frm){ + let options = ''; + + for(var link in frm.doc.dynamic_links){ + let dynamic_link = frm.doc.dynamic_links[link]; + options += '\n' + dynamic_link.link_doctype + ': ' + dynamic_link.link_name; + } + + var d = new frappe.ui.Dialog ({ + title: __("Remove link from Communication"), + fields: [{ + "fieldtype": "Select", + "options": options, + "label": __("Link"), + "fieldname": "link", + "reqd": 1 + }], + primary_action: ({ link }) => { + d.hide(); + frm.call('remove_link', { + link_doctype: link.split(":")[0].trim(), + link_name: link.split(":")[1].trim(), + autosave: true, + ignore_permissions: false + }).then(() => frm.refresh()); + }, + primary_action_label: __('Remove Link') + }); + d.show(); + }, + mark_as_read_unread: function(frm) { var action = frm.doc.seen? "Unread": "Read"; var flag = "(\\SEEN)"; @@ -185,7 +261,7 @@ frappe.ui.form.on("Communication", { forward_mail: function(frm) { var args = frm.events.get_mail_args(frm) - $.extend(args, { + $.extend(args, { forward: true, subject: __("Fw: {0}", [frm.doc.subject]), }) diff --git a/frappe/core/doctype/communication/communication.json b/frappe/core/doctype/communication/communication.json index 3b845964f4..b315a4ce98 100644 --- a/frappe/core/doctype/communication/communication.json +++ b/frappe/core/doctype/communication/communication.json @@ -1,7 +1,7 @@ { "allow_import": 1, "creation": "2013-01-29 10:47:14", - "description": "Keep a track of all communications", + "description": "Keeps track of all communications", "doctype": "DocType", "document_type": "Setup", "engine": "InnoDB", @@ -43,12 +43,11 @@ "email_template", "link_doctype", "link_name", - "timeline_doctype", - "timeline_name", - "timeline_label", "unread_notification_sent", "seen", "_user_tags", + "timeline_links_sections", + "dynamic_links", "email_inbox", "message_id", "uid", @@ -298,25 +297,6 @@ "options": "link_doctype", "read_only": 1 }, - { - "fieldname": "timeline_doctype", - "fieldtype": "Link", - "label": "Timeline DocType", - "options": "DocType", - "read_only": 1 - }, - { - "fieldname": "timeline_name", - "fieldtype": "Dynamic Link", - "label": "Timeline Name", - "options": "timeline_doctype", - "read_only": 1 - }, - { - "fieldname": "timeline_label", - "fieldtype": "Data", - "label": "Timeline field Name" - }, { "default": "0", "fieldname": "unread_notification_sent", @@ -398,11 +378,22 @@ "label": "Email Template", "options": "Email Template", "read_only": 1 + }, + { + "fieldname": "timeline_links_sections", + "fieldtype": "Section Break", + "label": "Timeline Links" + }, + { + "fieldname": "dynamic_links", + "fieldtype": "Table", + "label": "Dynamic Links", + "options": "Dynamic Link" } ], "icon": "fa fa-comment", "idx": 1, - "modified": "2019-05-04 15:36:35.818714", + "modified": "2019-05-13 19:55:35.757242", "modified_by": "Administrator", "module": "Core", "name": "Communication", diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index 77ccefba71..5952ae1fc4 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -8,7 +8,7 @@ from frappe.model.document import Document from frappe.utils import validate_email_address, get_fullname, strip_html, cstr from frappe.core.doctype.communication.email import (validate_email, notify, _notify, update_parent_mins_to_first_response) -from frappe.core.utils import get_parent_doc, set_timeline_doc +from frappe.core.utils import get_parent_doc from frappe.utils.bot import BotReply from frappe.utils import parse_addr from frappe.core.doctype.comment.comment import update_comment_in_doc @@ -58,7 +58,6 @@ class Communication(Document): self.set_sender_full_name() validate_email(self) - set_timeline_doc(self) def validate_reference(self): if self.reference_doctype and self.reference_name: @@ -231,26 +230,86 @@ class Communication(Document): if commit: frappe.db.commit() + # Timeline Links + def deduplicate_dynamic_links(self): + if self.dynamic_links: + links, duplicate = [], False + + for l in self.dynamic_links: + t = (l.link_doctype, l.link_name) + if not t in links: + links.append(t) + else: + duplicate = True + + if duplicate: + del self.dynamic_links[:] # make it python 2 compatible as list.clear() is python 3 only + for l in links: + self.add_link(link_doctype=l[0], link_name=l[1]) + + def validate_circular_links(self): + for dynamic_link in self.dynamic_links: + # Prevent circular linking of Communication DocTypes + if dynamic_link.link_doctype == "Communication": + circular_linking = False + circular_level_1 = get_timeline_parent_doc(dynamic_link.link_doctype, dynamic_link.link_name) + + # Level 1 + if circular_level_1: + for link in circular_level_1.dynamic_links: + if link.link_doctype == "Communication": + circular_level_2 = get_timeline_parent_doc(link.link_doctype, link.link_name) + + # Level 2 + if circular_level_2: + for ref_link in circular_level_2.dynamic_links: + if ref_link.link_doctype == "Communication": + circular_level_3 = get_timeline_parent_doc(ref_link.link_doctype, ref_link.link_name) + + # Level 3 + if circular_level_3: + if circular_level_3.name == self.name: + circular_linking = True + break + if circular_linking: + frappe.throw(_("Please make sure the Timeline Communication Docs are not circularly linked."), frappe.CircularLinkingError) + + def add_link(self, link_doctype, link_name, autosave=False): + self.append("dynamic_links", + { + "link_doctype": link_doctype, + "link_name": link_name + } + ) + + if autosave: + self.save(ignore_permissions=True) + + def get_links(self): + return self.dynamic_links + + def remove_link(self, link_doctype, link_name, autosave=False, ignore_permissions=True): + for l in self.dynamic_links: + if l.link_doctype == link_doctype and l.link_name == link_name: + self.dynamic_links.remove(l) + + if autosave: + self.save(ignore_permissions=ignore_permissions) def on_doctype_update(): """Add indexes in `tabCommunication`""" frappe.db.add_index("Communication", ["reference_doctype", "reference_name"]) - frappe.db.add_index("Communication", ["timeline_doctype", "timeline_name"]) frappe.db.add_index("Communication", ["link_doctype", "link_name"]) frappe.db.add_index("Communication", ["status", "communication_type"]) def has_permission(doc, ptype, user): if ptype=="read": - if (doc.reference_doctype == "Communication" and doc.reference_name == doc.name) \ - or (doc.timeline_doctype == "Communication" and doc.timeline_name == doc.name): - return + if doc.reference_doctype == "Communication" and doc.reference_name == doc.name: + return if doc.reference_doctype and doc.reference_name: if frappe.has_permission(doc.reference_doctype, ptype="read", doc=doc.reference_name): return True - if doc.timeline_doctype and doc.timeline_name: - if frappe.has_permission(doc.timeline_doctype, ptype="read", doc=doc.timeline_name): - return True def get_permission_query_conditions_for_communication(user): if not user: user = frappe.session.user @@ -270,3 +329,9 @@ def get_permission_query_conditions_for_communication(user): email_accounts = [ '"%s"'%account.get("email_account") for account in accounts ] return """tabCommunication.email_account in ({email_accounts})"""\ .format(email_accounts=','.join(email_accounts)) + +def get_timeline_parent_doc(link_doctype, link_name): + """Returns document of `link_doctype`, `link_name`""" + if link_doctype and link_name: + parent_doc = frappe.get_doc(link_doctype, link_name) + return parent_doc if parent_doc else None diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index 0c68f8b118..bc66223787 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -17,6 +17,7 @@ import frappe.email.smtp import time from frappe import _ from frappe.utils.background_jobs import enqueue +from email.utils import parseaddr @frappe.whitelist() def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent", @@ -78,6 +79,15 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = # if no reference given, then send it against the communication comm.db_set(dict(reference_doctype='Communication', reference_name=comm.name)) + # contacts = get_contacts([sender, recipients, cc, bcc]) + # for contact_name in contacts: + # comm.add_link('Contact', contact_name) + + # #link contact's dynamic links to communication + # add_contact_links_to_communication(comm, contact_name) + + # comm.save(ignore_permissions=True) + if isinstance(attachments, string_types): attachments = json.loads(attachments) @@ -559,3 +569,38 @@ def mark_email_as_seen(name=None): frappe.response["filename"] = "imaginary_pixel.png" frappe.response["filecontent"] = buffered_obj.getvalue() +def get_contacts(email_strings): + email_addrs = [] + + for email_string in email_strings: + if email_string: + for email in email_string.split(","): + parsed_email = parseaddr(email)[1] + if parsed_email: + email_addrs.append(parsed_email) + + contacts = [] + for email in email_addrs: + contact_name = frappe.db.get_value('Contact', {'email_id': email}) + + if not contact_name: + contact = frappe.get_doc({ + "doctype": "Contact", + "first_name": frappe.unscrub(email.split("@")[0]), + "email_id": email + }).insert(ignore_permissions=True) + contact_name = contact.name + + contacts.append(contact_name) + + return contacts + +def add_contact_links_to_communication(communication, contact_name): + contact_links = frappe.get_list("Dynamic Link", filters={ + "parenttype": "Contact", + "parent": contact_name + }, fields=["link_doctype", "link_name"]) + + if contact_links: + for contact_link in contact_links: + communication.add_link(contact_link.link_doctype, contact_link.link_name) \ No newline at end of file diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 14ebbaf7fb..dd6a559dc1 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -161,36 +161,55 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields= group_by=None, as_dict=True): '''Returns list of communications for a given document''' if not fields: - fields = '''`name`, `communication_type`,`communication_medium`, `comment_type`, - `communication_date`, `content`, `sender`, `sender_full_name`, `cc`, `bcc`, - `creation`, `subject`, `delivery_status`, `_liked_by`, - `timeline_doctype`, `timeline_name`, `reference_doctype`, `reference_name`, - `link_doctype`, `link_name`, `read_by_recipient`, `rating`, 'Communication' AS `doctype`''' + fields = ''' + `tabCommunication`.name, `tabCommunication`.communication_type, `tabCommunication`.communication_medium, + `tabCommunication`.comment_type, `tabCommunication`.communication_date, `tabCommunication`.content, + `tabCommunication`.sender, `tabCommunication`.sender_full_name, `tabCommunication`.cc, `tabCommunication`.bcc, + `tabCommunication`.creation, `tabCommunication`.subject, `tabCommunication`.delivery_status, + `tabCommunication`._liked_by, `tabCommunication`.reference_doctype, `tabCommunication`.reference_name, + `tabCommunication`.link_doctype, `tabCommunication`.link_name, `tabCommunication`.read_by_recipient, + `tabCommunication`.rating, `tabDynamic Link`.link_doctype, `tabDynamic Link`.link_name + ''' - conditions = '''communication_type in ('Communication', 'Feedback') - and ( - (reference_doctype=%(doctype)s and reference_name=%(name)s) + conditions = ''' + `tabCommunication`.communication_type in ('Communication', 'Feedback') + and ( + (`tabCommunication`.reference_doctype=%(doctype)s and `tabCommunication`.reference_name=%(name)s) or ( - (timeline_doctype=%(doctype)s and timeline_name=%(name)s) - and (communication_type='Communication') + (`tabDynamic Link`.link_doctype=%(doctype)s and `tabDynamic Link`.link_name=%(name)s) + and (`tabCommunication`.communication_type='Communication') ) - )''' - + ) + ''' if after: # find after a particular date - conditions+= ' and creation > {0}'.format(after) + conditions += ''' + and `tabCommunication`.creation > {0} + '''.format(after) if doctype=='User': - conditions+= " and not (reference_doctype='User' and communication_type='Communication')" + conditions += ''' + and not (`tabCommunication`.reference_doctype='User' and `tabCommunication`.communication_type='Communication') + ''' - communications = frappe.db.sql("""select {fields} + communications = frappe.db.sql(''' + select {fields} from `tabCommunication` + left join `tabDynamic Link` + on `tabCommunication`.name=`tabDynamic Link`.parent where {conditions} {group_by} - order by creation desc LIMIT %(limit)s OFFSET %(start)s""".format( - fields = fields, conditions=conditions, group_by=group_by or ""), - { "doctype": doctype, "name": name, "start": frappe.utils.cint(start), "limit": limit }, - as_dict=as_dict) + order by `tabCommunication`.creation desc + limit %(limit)s offset %(start)s'''.format( + fields = fields, + conditions=conditions, + group_by=group_by or "" + ),{ + "doctype": doctype, + "name": name, + "start": frappe.utils.cint(start), + "limit": limit + }, as_dict=as_dict, debug=True) return communications @@ -245,4 +264,4 @@ def get_view_logs(doctype, docname): if view_logs: logs = view_logs - return logs + return logs \ No newline at end of file diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 2d23089f5a..1dd8aa284c 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -20,7 +20,7 @@ from datetime import datetime, timedelta from frappe.desk.form import assign_to from frappe.utils.user import get_system_managers from frappe.utils.background_jobs import enqueue, get_jobs -from frappe.core.doctype.communication.email import set_incoming_outgoing_accounts +from frappe.core.doctype.communication.email import set_incoming_outgoing_accounts, get_contacts, add_contact_links_to_communication from frappe.utils.scheduler import log from frappe.utils.html_utils import clean_email_html @@ -386,6 +386,13 @@ class EmailAccount(Document): users = list(set([ user.get("parent") for user in users ])) communication._seen = json.dumps(users) + # contacts = get_contacts([sender, recipients, cc, bcc]) + # for contact_name in contacts: + # comm.add_link('Contact', contact_name) + + # #link contact's dynamic links to communication + # add_contact_links_to_communication(comm, contact_name) + communication.flags.in_receive = True communication.insert(ignore_permissions = 1) From 5d09fb26a9085ca436a267cb88c4a8da396bc8bb Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Tue, 14 May 2019 12:01:14 +0530 Subject: [PATCH 06/60] fix: timeline docs to dynamic links --- .../core/doctype/activity_log/activity_log.py | 2 +- .../doctype/communication/communication.py | 57 ++++++++----------- frappe/core/doctype/communication/email.py | 18 +++--- .../communication/test_communication.py | 12 ++-- frappe/desk/doctype/event/event.py | 33 ++++++++--- frappe/desk/form/load.py | 7 ++- .../doctype/email_account/email_account.py | 12 ++-- .../email_account/test_email_account.py | 27 ++++++--- frappe/model/delete_doc.py | 11 +++- 9 files changed, 106 insertions(+), 73 deletions(-) diff --git a/frappe/core/doctype/activity_log/activity_log.py b/frappe/core/doctype/activity_log/activity_log.py index 7badf737e4..8b7941c086 100644 --- a/frappe/core/doctype/activity_log/activity_log.py +++ b/frappe/core/doctype/activity_log/activity_log.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals from frappe import _ from frappe.utils import get_fullname, now from frappe.model.document import Document -from frappe.core.utils import get_parent_doc, set_timeline_doc +from frappe.core.utils import set_timeline_doc import frappe class ActivityLog(Document): diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index 5952ae1fc4..f25264ceb3 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -8,7 +8,6 @@ from frappe.model.document import Document from frappe.utils import validate_email_address, get_fullname, strip_html, cstr from frappe.core.doctype.communication.email import (validate_email, notify, _notify, update_parent_mins_to_first_response) -from frappe.core.utils import get_parent_doc from frappe.utils.bot import BotReply from frappe.utils import parse_addr from frappe.core.doctype.comment.comment import update_comment_in_doc @@ -57,6 +56,8 @@ class Communication(Document): self.set_status() self.set_sender_full_name() + self.validate_dynamic_links() + self.deduplicate_dynamic_links() validate_email(self) def validate_reference(self): @@ -72,12 +73,14 @@ class Communication(Document): # Prevent circular linking of Communication DocTypes if self.reference_doctype == "Communication": circular_linking = False - doc = get_parent_doc(self) - while doc.reference_doctype == "Communication": - if get_parent_doc(doc).name==self.name: - circular_linking = True - break - doc = get_parent_doc(doc) + doc = get_parent_doc(self.reference_doctype, self.reference_name) + if doc: + while doc.reference_doctype == "Communication": + if doc: + if doc.reference_name == self.name: + circular_linking = True + break + doc = get_parent_doc(doc.reference_doctype, doc.reference_name) if circular_linking: frappe.throw(_("Please make sure the Reference Communication Docs are not circularly linked."), frappe.CircularLinkingError) @@ -247,32 +250,22 @@ class Communication(Document): for l in links: self.add_link(link_doctype=l[0], link_name=l[1]) - def validate_circular_links(self): + def validate_dynamic_links(self): + circular_linking = False for dynamic_link in self.dynamic_links: - # Prevent circular linking of Communication DocTypes + + # Prevent circular linking of Timeline DocTypes if dynamic_link.link_doctype == "Communication": - circular_linking = False - circular_level_1 = get_timeline_parent_doc(dynamic_link.link_doctype, dynamic_link.link_name) + doc = get_parent_doc(dynamic_link.link_doctype, dynamic_link.link_name) + if doc: + while doc.reference_doctype == "Communication": + if doc: + if doc.reference_name == self.name: + circular_linking = True + break - # Level 1 - if circular_level_1: - for link in circular_level_1.dynamic_links: - if link.link_doctype == "Communication": - circular_level_2 = get_timeline_parent_doc(link.link_doctype, link.link_name) - - # Level 2 - if circular_level_2: - for ref_link in circular_level_2.dynamic_links: - if ref_link.link_doctype == "Communication": - circular_level_3 = get_timeline_parent_doc(ref_link.link_doctype, ref_link.link_name) - - # Level 3 - if circular_level_3: - if circular_level_3.name == self.name: - circular_linking = True - break - if circular_linking: - frappe.throw(_("Please make sure the Timeline Communication Docs are not circularly linked."), frappe.CircularLinkingError) + if circular_linking: + frappe.throw(_("Please make sure the Timeline Communication Docs are not circularly linked."), frappe.CircularLinkingError) def add_link(self, link_doctype, link_name, autosave=False): self.append("dynamic_links", @@ -330,8 +323,8 @@ def get_permission_query_conditions_for_communication(user): return """tabCommunication.email_account in ({email_accounts})"""\ .format(email_accounts=','.join(email_accounts)) -def get_timeline_parent_doc(link_doctype, link_name): +def get_parent_doc(link_doctype, link_name): """Returns document of `link_doctype`, `link_name`""" if link_doctype and link_name: parent_doc = frappe.get_doc(link_doctype, link_name) - return parent_doc if parent_doc else None + return parent_doc if parent_doc else None \ No newline at end of file diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index bc66223787..e554e5f7cf 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -72,21 +72,21 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "message_id":get_message_id().strip(" <>"), "read_receipt":read_receipt, "has_attachment": 1 if attachments else 0 - }) - comm.insert(ignore_permissions=True) + }).insert(ignore_permissions=True) if not doctype: # if no reference given, then send it against the communication - comm.db_set(dict(reference_doctype='Communication', reference_name=comm.name)) + comm.reference_doctype = 'Communication' + comm.reference_name = comm.name - # contacts = get_contacts([sender, recipients, cc, bcc]) - # for contact_name in contacts: - # comm.add_link('Contact', contact_name) + contacts = get_contacts([sender, recipients, cc, bcc]) + for contact_name in contacts: + comm.add_link('Contact', contact_name) - # #link contact's dynamic links to communication - # add_contact_links_to_communication(comm, contact_name) + #link contact's dynamic links to communication + add_contact_links_to_communication(comm, contact_name) - # comm.save(ignore_permissions=True) + comm.save(ignore_permissions=True) if isinstance(attachments, string_types): attachments = json.loads(attachments) diff --git a/frappe/core/doctype/communication/test_communication.py b/frappe/core/doctype/communication/test_communication.py index 1941ff31cc..53b0981af7 100644 --- a/frappe/core/doctype/communication/test_communication.py +++ b/frappe/core/doctype/communication/test_communication.py @@ -44,28 +44,30 @@ class TestCommunication(unittest.TestCase): self.assertFalse(frappe.utils.parse_addr(x)[0]) def test_circular_linking(self): - content = "This was created to test circular linking" a = frappe.get_doc({ "doctype": "Communication", "communication_type": "Communication", - "content": content, + "content": "This was created to test circular linking: Communication A", }).insert() + b = frappe.get_doc({ "doctype": "Communication", "communication_type": "Communication", - "content": content, + "content": "This was created to test circular linking: Communication B", "reference_doctype": "Communication", "reference_name": a.name }).insert() + c = frappe.get_doc({ "doctype": "Communication", "communication_type": "Communication", - "content": content, + "content": "This was created to test circular linking: Communication C", "reference_doctype": "Communication", "reference_name": b.name }).insert() + a = frappe.get_doc("Communication", a.name) a.reference_doctype = "Communication" a.reference_name = c.name - self.assertRaises(frappe.CircularLinkingError, a.save) + self.assertRaises(frappe.CircularLinkingError, a.save) diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py index 04f7455e2d..db601b418c 100644 --- a/frappe/desk/doctype/event/event.py +++ b/frappe/desk/doctype/event/event.py @@ -43,10 +43,17 @@ class Event(Document): def sync_communication(self): if self.event_participants: for participant in self.event_participants: - communication_name = frappe.db.get_value("Communication", dict(reference_doctype=self.doctype, reference_name=self.name, timeline_doctype=participant.reference_doctype, timeline_name=participant.reference_docname), "name") - if communication_name: - communication = frappe.get_doc("Communication", communication_name) - self.update_communication(participant, communication) + comms = frappe.get_list("Communication", filters=[ + ["Communication", "reference_doctype", "=", self.doctype], + ["Communication", "reference_name", "=", self.name], + ["Dynamic Link", "link_doctype", "=", participant.reference_doctype] + ["Dynamic Link", "link_name", "=", participant.reference_docname] + ], fields=["name"]) + + if comms: + for comm in comms: + communication = frappe.get_doc("Communication", comms.name) + self.update_communication(participant, communication) else: meta = frappe.get_meta(participant.reference_doctype) if hasattr(meta, "allow_events_in_timeline") and meta.allow_events_in_timeline==1: @@ -62,12 +69,11 @@ class Event(Document): communication.subject = self.subject communication.content = self.description if self.description else self.subject communication.communication_date = self.starts_on - communication.timeline_doctype = participant.reference_doctype - communication.timeline_name = participant.reference_docname communication.reference_doctype = self.doctype communication.reference_name = self.name communication.communication_medium = communication_mapping[self.event_category] if self.event_category else "" communication.status = "Linked" + communication.add_link(participant.reference_doctype, participant.reference_docname) communication.save(ignore_permissions=True) @frappe.whitelist() @@ -76,9 +82,18 @@ def delete_communication(event, reference_doctype, reference_docname): if isinstance(event, string_types): event = json.loads(event) - communication_name = frappe.db.get_value("Communication", dict(reference_doctype=event["doctype"], reference_name=event["name"], timeline_doctype=deleted_participant.reference_doctype, timeline_name=deleted_participant.reference_docname), "name") - if communication_name: - deletion = frappe.get_doc("Communication", communication_name).delete() + comms = frappe.get_list("Communication", filters=[ + ["Communication", "reference_doctype", "=", event.get("doctype")], + ["Communication", "reference_name", "=", event.get("name")], + ["Dynamic Link", "link_doctype", "=", deleted_participant.reference_doctype], + ["Dynamic Link", "link_name", "=", deleted_participant.reference_docname] + ], fields=["name"]) + + if comms: + deletion = [] + for comm in comms: + delete = frappe.get_doc("Communication", comm.name).delete() + deletion.append(delete) return deletion diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index dd6a559dc1..0d986e5bc0 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -182,6 +182,11 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields= ) ''' + if not group_by: + group_by = ''' + group by `tabCommunication`.name + ''' + if after: # find after a particular date conditions += ''' @@ -210,7 +215,7 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields= "start": frappe.utils.cint(start), "limit": limit }, as_dict=as_dict, debug=True) - + print(communications) return communications def get_assignments(dt, dn): diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 1dd8aa284c..fa8b409b1f 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -386,15 +386,15 @@ class EmailAccount(Document): users = list(set([ user.get("parent") for user in users ])) communication._seen = json.dumps(users) - # contacts = get_contacts([sender, recipients, cc, bcc]) - # for contact_name in contacts: - # comm.add_link('Contact', contact_name) + contacts = get_contacts([email.from_email, email.mail.get("To"), email.mail.get("CC"), email.from_email]) + for contact_name in contacts: + communication.add_link('Contact', contact_name) - # #link contact's dynamic links to communication - # add_contact_links_to_communication(comm, contact_name) + #link contact's dynamic links to communication + add_contact_links_to_communication(communication, contact_name) communication.flags.in_receive = True - communication.insert(ignore_permissions = 1) + communication.insert(ignore_permissions=True) # save attachments communication._attachments = email.save_attachments_in_doc(communication) diff --git a/frappe/email/doctype/email_account/test_email_account.py b/frappe/email/doctype/email_account/test_email_account.py index f098a8b205..ac16f12477 100644 --- a/frappe/email/doctype/email_account/test_email_account.py +++ b/frappe/email/doctype/email_account/test_email_account.py @@ -26,7 +26,7 @@ class TestEmailAccount(unittest.TestCase): email_account.db_set("enable_incoming", 0) def test_incoming(self): - frappe.db.sql("delete from tabCommunication where sender='test_sender@example.com'") + cleanup("test_sender@example.com") with open(os.path.join(os.path.dirname(__file__), "test_mails", "incoming-1.raw"), "r") as f: test_mails = [f.read()] @@ -52,7 +52,8 @@ class TestEmailAccount(unittest.TestCase): "reference_name": comm.reference_name, "status":"Not Sent"})) def test_incoming_with_attach(self): - frappe.db.sql("DELETE FROM `tabCommunication` WHERE sender='test_sender@example.com'") + cleanup("test_sender@example.com") + existing_file = frappe.get_doc({'doctype': 'File', 'file_name': 'erpnext-conf-14.png'}) frappe.delete_doc("File", existing_file.name) @@ -75,7 +76,7 @@ class TestEmailAccount(unittest.TestCase): def test_incoming_attached_email_from_outlook_plain_text_only(self): - frappe.db.sql("delete from tabCommunication where sender='test_sender@example.com'") + cleanup("test_sender@example.com") with open(os.path.join(os.path.dirname(__file__), "test_mails", "incoming-3.raw"), "r") as f: test_mails = [f.read()] @@ -88,7 +89,7 @@ class TestEmailAccount(unittest.TestCase): self.assertTrue("This is an e-mail message sent automatically by Microsoft Outlook while" in comm.content) def test_incoming_attached_email_from_outlook_layers(self): - frappe.db.sql("delete from tabCommunication where sender='test_sender@example.com'") + cleanup("test_sender@example.com") with open(os.path.join(os.path.dirname(__file__), "test_mails", "incoming-4.raw"), "r") as f: test_mails = [f.read()] @@ -123,8 +124,7 @@ class TestEmailAccount(unittest.TestCase): self.assertTrue("test-mail-002" in sent_mail.get("Subject")) def test_threading(self): - frappe.db.sql("""delete from tabCommunication - where sender in ('test_sender@example.com', 'test@example.com')""") + cleanup(["in", ['test_sender@example.com', 'test@example.com']]) # send sent_name = make(subject = "Test", content="test content", @@ -149,8 +149,7 @@ class TestEmailAccount(unittest.TestCase): self.assertEqual(comm.reference_name, sent.reference_name) def test_threading_by_subject(self): - frappe.db.sql("""delete from tabCommunication - where sender in ('test_sender@example.com', 'test@example.com')""") + cleanup(["in", ['test_sender@example.com', 'test@example.com']]) with open(os.path.join(os.path.dirname(__file__), "test_mails", "reply-2.raw"), "r") as f: test_mails = [f.read()] @@ -170,7 +169,7 @@ class TestEmailAccount(unittest.TestCase): self.assertEqual(comm_list[0].reference_name, comm_list[1].reference_name) def test_threading_by_message_id(self): - frappe.db.sql("""delete from tabCommunication""") + cleanup() frappe.db.sql("""delete from `tabEmail Queue`""") # reference document for testing @@ -196,3 +195,13 @@ class TestEmailAccount(unittest.TestCase): # check if threaded correctly self.assertEqual(comm_list[0].reference_doctype, event.doctype) self.assertEqual(comm_list[0].reference_name, event.name) + +def cleanup(sender=None): + filters = {} + if sender: + filters.update({"sender": sender}) + + names = frappe.get_list("Communication", filters=filters, fields=["name"]) + for name in names: + frappe.delete_doc_if_exists("Communication", name.name) + frappe.delete_doc_if_exists("Dynamic Link", {"parent": name.name}) \ No newline at end of file diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index ab15dab74e..85d7a3c72b 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -279,7 +279,7 @@ def delete_dynamic_links(doctype, name): # unlink communications clear_references('Communication', doctype, name) clear_references('Communication', doctype, name, 'link_doctype', 'link_name') - clear_references('Communication', doctype, name, 'timeline_doctype', 'timeline_name') + clear_timeline_references(doctype, name) clear_references('Activity Log', doctype, name) clear_references('Activity Log', doctype, name, 'timeline_doctype', 'timeline_name') @@ -300,6 +300,15 @@ def clear_references(doctype, reference_doctype, reference_name, {1}=%s and {2}=%s'''.format(doctype, reference_doctype_field, reference_name_field), # nosec (reference_doctype, reference_name)) +def clear_timeline_references(link_doctype, link_name): + comms = frappe.get_list("Communication", filters=[ + ["Dynamic Link", "link_doctype", "=", link_doctype], + ["Dynamic Link", "link_name", "=", link_name] + ], fields=["name"]) + + for comm in comms: + doc = frappe.get_doc("Communication", comm.name) + doc.remove_link(link_doctype=link_doctype, link_name=link_name, autosave=True) def insert_feed(doc): from frappe.utils import get_fullname From ee27bd80c8c452460d12bd5aebd58bf37ce091b7 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Tue, 14 May 2019 12:58:16 +0530 Subject: [PATCH 07/60] fix: typo --- frappe/desk/doctype/event/event.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py index db601b418c..d99cc64436 100644 --- a/frappe/desk/doctype/event/event.py +++ b/frappe/desk/doctype/event/event.py @@ -46,13 +46,13 @@ class Event(Document): comms = frappe.get_list("Communication", filters=[ ["Communication", "reference_doctype", "=", self.doctype], ["Communication", "reference_name", "=", self.name], - ["Dynamic Link", "link_doctype", "=", participant.reference_doctype] + ["Dynamic Link", "link_doctype", "=", participant.reference_doctype], ["Dynamic Link", "link_name", "=", participant.reference_docname] ], fields=["name"]) if comms: for comm in comms: - communication = frappe.get_doc("Communication", comms.name) + communication = frappe.get_doc("Communication", comm.name) self.update_communication(participant, communication) else: meta = frappe.get_meta(participant.reference_doctype) From 8828fcef24d8cb5608bc22802c2f5a54ec474e66 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Tue, 14 May 2019 17:22:24 +0530 Subject: [PATCH 08/60] fix: use inner join instead of left join --- frappe/desk/form/load.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 9c05e5c05a..b6277a5c31 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -167,7 +167,7 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields= `tabCommunication`.creation, `tabCommunication`.subject, `tabCommunication`.delivery_status, `tabCommunication`._liked_by, `tabCommunication`.reference_doctype, `tabCommunication`.reference_name, `tabCommunication`.link_doctype, `tabCommunication`.link_name, `tabCommunication`.read_by_recipient, - `tabCommunication`.rating, `tabDynamic Link`.link_doctype, `tabDynamic Link`.link_name + `tabCommunication`.rating, `tabDynamic Link`.link_doctype, `tabDynamic Link`.link_name, `tabDynamic Link`.parent ''' conditions = ''' @@ -183,7 +183,7 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields= if not group_by: group_by = ''' - group by `tabCommunication`.name + group by `tabCommunication`.name, `tabDynamic Link`.parent ''' if after: @@ -200,7 +200,7 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields= communications = frappe.db.sql(''' select {fields} from `tabCommunication` - left join `tabDynamic Link` + inner join `tabDynamic Link` on `tabCommunication`.name=`tabDynamic Link`.parent where {conditions} {group_by} order by `tabCommunication`.creation desc From d230bf4cea1cc5af7c7e60839b5aa9844b1be7b8 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Tue, 14 May 2019 17:39:55 +0530 Subject: [PATCH 09/60] patch: migarte timeline links to dynamic links --- .../doctype/dynamic_link/dynamic_link.json | 163 +++++------------- frappe/patches.txt | 3 + .../move_timeline_links_to_dynamic_links.py | 25 +++ 3 files changed, 73 insertions(+), 118 deletions(-) create mode 100644 frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py diff --git a/frappe/core/doctype/dynamic_link/dynamic_link.json b/frappe/core/doctype/dynamic_link/dynamic_link.json index 3689be6a3d..a8b871855c 100644 --- a/frappe/core/doctype/dynamic_link/dynamic_link.json +++ b/frappe/core/doctype/dynamic_link/dynamic_link.json @@ -1,125 +1,52 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-01-13 04:55:18.835023", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "creation": "2017-01-13 04:55:18.835023", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "link_doctype", + "link_title", + "cb_00", + "link_name" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "link_doctype", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Link 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, - "unique": 0 - }, + "fieldname": "link_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Link DocType", + "options": "DocType", + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "link_name", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Link Name", - "length": 0, - "no_copy": 0, - "options": "link_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, - "unique": 0 - }, + "fieldname": "link_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Link Name", + "options": "link_doctype", + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "link_title", - "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Link Title", - "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": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "link_title", + "fieldtype": "Read Only", + "in_list_view": 1, + "label": "Link Title", + "read_only": 1 + }, + { + "fieldname": "cb_00", + "fieldtype": "Column Break" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2017-01-17 14:25:49.140730", - "modified_by": "Administrator", - "module": "Core", - "name": "Dynamic Link", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 + ], + "istable": 1, + "modified": "2019-05-14 17:36:38.391805", + "modified_by": "Administrator", + "module": "Core", + "name": "Dynamic Link", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/patches.txt b/frappe/patches.txt index c0b2a8238f..15f9ad8c73 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -241,3 +241,6 @@ frappe.patches.v12_0.reset_home_settings frappe.patches.v12_0.update_print_format_type frappe.patches.v11_0.remove_doctype_user_permissions_for_page_and_report #2019-05-01 frappe.patches.v12_0.remove_feedback_rating +execute:frappe.reload_doc('core', 'doctype', 'communication') +execute:frappe.reload_doc('core', 'doctype', 'dynamic_link') +frappe.patches.v12_0.move_timeline_links_to_dynamic_links \ No newline at end of file diff --git a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py new file mode 100644 index 0000000000..0bb266269e --- /dev/null +++ b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py @@ -0,0 +1,25 @@ +from __future__ import unicode_literals + +import frappe + +def execute(): + comm_lists = [] + for communication in frappe.get_list("Communication", filters={"communication_medium": "Email"}, + fields=[ + "name", "creation", "modified", "modified_by", + "timeline_doctype", "timeline_name", + ]): + if communication.timeline_doctype and communication.timeline_name: + comm_lists.append(( + "1", frappe.generate_hash(length=10), "dynamic_links", "Communication", + communication.name, communication.timeline_doctype, communication.timeline_name, + communication.creation, communication.modified, communication.modified_by + )) + + for comm_list in comm_lists: + frappe.db.sql(""" + insert into table `tabDynamic Link` (idx, name, parentfield, parenttype, parent, link_doctype, link_name, creation, modified, modified_by) + values %(values)s""", + { + "values": comm_list + }) \ No newline at end of file From e29f07898b2436db96e5325e447b3101ad7b022e Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Wed, 15 May 2019 11:29:22 +0530 Subject: [PATCH 10/60] fix: query --- frappe/desk/form/load.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index b6277a5c31..6d834b3b12 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -167,7 +167,7 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields= `tabCommunication`.creation, `tabCommunication`.subject, `tabCommunication`.delivery_status, `tabCommunication`._liked_by, `tabCommunication`.reference_doctype, `tabCommunication`.reference_name, `tabCommunication`.link_doctype, `tabCommunication`.link_name, `tabCommunication`.read_by_recipient, - `tabCommunication`.rating, `tabDynamic Link`.link_doctype, `tabDynamic Link`.link_name, `tabDynamic Link`.parent + `tabCommunication`.rating ''' conditions = ''' @@ -181,11 +181,6 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields= ) ''' - if not group_by: - group_by = ''' - group by `tabCommunication`.name, `tabDynamic Link`.parent - ''' - if after: # find after a particular date conditions += ''' @@ -198,17 +193,13 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields= ''' communications = frappe.db.sql(''' - select {fields} + select distinct {fields} from `tabCommunication` inner join `tabDynamic Link` on `tabCommunication`.name=`tabDynamic Link`.parent where {conditions} {group_by} order by `tabCommunication`.creation desc - limit %(limit)s offset %(start)s'''.format( - fields = fields, - conditions=conditions, - group_by=group_by or "" - ),{ + limit %(limit)s offset %(start)s'''.format(fields = fields, conditions=conditions, group_by=group_by or ""),{ "doctype": doctype, "name": name, "start": frappe.utils.cint(start), From a0b1ae732f66dc8f574783a8c0b9f31956ae3bc7 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 15 May 2019 11:39:31 +0530 Subject: [PATCH 11/60] fix: Add currency code for Surinamese dollar --- frappe/geo/country_info.json | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/geo/country_info.json b/frappe/geo/country_info.json index bdd6414730..7a6ba56929 100644 --- a/frappe/geo/country_info.json +++ b/frappe/geo/country_info.json @@ -2330,6 +2330,7 @@ }, "Suriname": { "code": "sr", + "currency": "SRD", "currency_fraction": "Cent", "currency_fraction_units": 100, "currency_symbol": "$", From 1bcdc0b7cb2cf110b2e3fad7f1e8c712866817b0 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 15 May 2019 11:59:13 +0530 Subject: [PATCH 12/60] fix(report): Allow report export only if user has export permission on ref doctype (#7458) * fix: Allow export only if user has export permission on reference doctype * fix: Show only custom "no permission" error * fix: while saving employee user getting user permissions error --- frappe/desk/query_report.py | 4 ++++ frappe/model/rename_doc.py | 2 +- frappe/permissions.py | 6 ++++-- frappe/public/js/frappe/views/reports/query_report.js | 1 + 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 561cd680b6..0890e2ad7a 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -282,6 +282,10 @@ def export_query(): filters = json.loads(data["filters"]) if isinstance(data.get("report_name"), string_types): report_name = data["report_name"] + frappe.permissions.can_export( + frappe.get_cached_value('Report', report_name, 'ref_doctype'), + raise_exception=True + ) if isinstance(data.get("file_format_type"), string_types): file_format_type = data["file_format_type"] diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index a373554696..12c57f2780 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -161,7 +161,7 @@ def validate_rename(doctype, new, meta, merge, force, ignore_permissions): if (not merge) and exists: frappe.msgprint(_("Another {0} with name {1} exists, select another name").format(doctype, new), raise_exception=1) - if not (ignore_permissions or frappe.has_permission(doctype, "write")): + if not (ignore_permissions or frappe.permissions.has_permission(doctype, "write", raise_exception=False)): frappe.msgprint(_("You need write permission to rename"), raise_exception=1) if not (force or ignore_permissions) and not meta.allow_rename: diff --git a/frappe/permissions.py b/frappe/permissions.py index 3809bb3e9c..e5aa31d139 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -25,16 +25,18 @@ def print_has_permission_check_logs(func): frappe.flags['has_permission_check_logs'] = [] result = func(*args, **kwargs) self_perm_check = True if not kwargs.get('user') else kwargs.get('user') == frappe.session.user + raise_exception = False if kwargs.get('raise_exception') == False else True + # print only if access denied # and if user is checking his own permission - if not result and self_perm_check: + if not result and self_perm_check and raise_exception: msgprint(('
').join(frappe.flags.get('has_permission_check_logs'))) frappe.flags.pop('has_permission_check_logs', None) return result return inner @print_has_permission_check_logs -def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None): +def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None, raise_exception=True): """Returns True if user has permission `ptype` for given `doctype`. If `doc` is passed, it also checks user, share and owner permissions. diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 485f26f952..7baff2a25e 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -970,6 +970,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { { label: __('Export'), action: () => this.export_report(), + condition: () => frappe.model.can_export(this.report_doc.ref_doctype), standard: true }, { From 2ba5438c5fe3a391217c9bdbb038e04cfab10876 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 15 May 2019 13:47:33 +0530 Subject: [PATCH 13/60] feat: Allow multiple apps to be installed to site Usage: `bench --site sitename install-app erpnext foundation` --- frappe/commands/site.py | 9 +++++---- frappe/public/js/frappe/roles_editor.js | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 3798c07e91..ee8131c1dc 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -157,16 +157,17 @@ def _reinstall(site, admin_password=None, mariadb_root_username=None, mariadb_ro admin_password=admin_password) @click.command('install-app') -@click.argument('app') +@click.argument('apps', nargs=-1) @pass_context -def install_app(context, app): - "Install a new app to site" +def install_app(context, apps): + "Install a new app to site, supports multiple apps" from frappe.installer import install_app as _install_app for site in context.sites: frappe.init(site=site) frappe.connect() try: - _install_app(app, verbose=context.verbose) + for app in apps: + _install_app(app, verbose=context.verbose) finally: frappe.destroy() diff --git a/frappe/public/js/frappe/roles_editor.js b/frappe/public/js/frappe/roles_editor.js index 0e5a85be68..50a3db776c 100644 --- a/frappe/public/js/frappe/roles_editor.js +++ b/frappe/public/js/frappe/roles_editor.js @@ -183,7 +183,7 @@ frappe.RoleEditor = Class.extend({ %(set_user_permissions)s\ ', perm)); } - + me.perm_dialog.set_title(role); me.perm_dialog.show(); } }); @@ -191,7 +191,7 @@ frappe.RoleEditor = Class.extend({ }, make_perm_dialog: function() { this.perm_dialog = new frappe.ui.Dialog({ - title:__('Role Permissions') + title: __('Role Permissions') }); this.perm_dialog.$wrapper.find('.modal-dialog').css("width", "800px"); From 25b190bb9cde3ee7d41b16b268d5d1296346dc08 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Wed, 15 May 2019 13:48:21 +0530 Subject: [PATCH 14/60] fix: permission fix for prepared report --- frappe/core/doctype/prepared_report/prepared_report.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py index 847f1a840b..29c069515f 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.py +++ b/frappe/core/doctype/prepared_report/prepared_report.py @@ -54,14 +54,14 @@ def run_background(prepared_report): instance.status = "Completed" instance.columns = json.dumps(result["columns"]) instance.report_end_time = frappe.utils.now() - instance.save() + instance.save(ignore_permissions=True) except Exception: frappe.log_error(frappe.get_traceback()) instance = frappe.get_doc("Prepared Report", prepared_report) instance.status = "Error" instance.error_message = frappe.get_traceback() - instance.save() + instance.save(ignore_permissions=True) frappe.publish_realtime( 'report_generated', From 77d009747d30a4df3ff8ad3036121dfc4a1af36d Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Wed, 15 May 2019 14:00:25 +0530 Subject: [PATCH 15/60] fix: remove debug parameter --- frappe/desk/form/load.py | 4 ++-- frappe/model/delete_doc.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 6d834b3b12..102972f331 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -204,8 +204,8 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields= "name": name, "start": frappe.utils.cint(start), "limit": limit - }, as_dict=as_dict, debug=True) - print(communications) + }, as_dict=as_dict) + return communications def get_assignments(dt, dn): diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 351cd5ca30..685f295b10 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -307,9 +307,10 @@ def clear_timeline_references(link_doctype, link_name): ["Dynamic Link", "link_name", "=", link_name] ], fields=["name"]) - for comm in comms: - doc = frappe.get_doc("Communication", comm.name) - doc.remove_link(link_doctype=link_doctype, link_name=link_name, autosave=True) + if comms: + for comm in comms: + doc = frappe.get_doc("Communication", comm.name) + doc.remove_link(link_doctype=link_doctype, link_name=link_name, autosave=True, ignore_permissions=True) def insert_feed(doc): from frappe.utils import get_fullname From 7c8cd45bbf284874aa2146797aa1e1bb3508363a Mon Sep 17 00:00:00 2001 From: cameron Date: Wed, 15 May 2019 16:35:00 +0800 Subject: [PATCH 16/60] ldap3 changes --- .../doctype/ldap_settings/ldap_settings.json | 844 +++++++++++------- .../doctype/ldap_settings/ldap_settings.py | 196 ++-- frappe/templates/includes/login/login.js | 2 +- frappe/www/login.py | 4 +- 4 files changed, 615 insertions(+), 431 deletions(-) diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.json b/frappe/integrations/doctype/ldap_settings/ldap_settings.json index 6eb44a2db8..aa43b2e9d0 100644 --- a/frappe/integrations/doctype/ldap_settings/ldap_settings.json +++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.json @@ -1,317 +1,363 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-09-22 04:16:48.829658", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "System", - "editable_grid": 1, + "allow_copy": 0, + "allow_events_in_timeline": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2016-09-22 04:16:48.829658", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "System", + "editable_grid": 1, "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "enabled", - "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": "Enabled", - "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, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "ldap_server_url", - "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": "LDAP Server Url", - "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, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "organizational_unit", - "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": "Organizational Unit", - "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, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "base_dn", - "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": "Base Distinguished Name (DN)", - "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, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "password", - "fieldtype": "Password", - "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": "Password for Base DN", - "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, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_5", - "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, - "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, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "ldap_search_string", - "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": "LDAP Search String", - "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, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "ldap_first_name_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": "LDAP First Name Field", - "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, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "ldap_email_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": "LDAP Email Field", - "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, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "ldap_username_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": "LDAP Username Field", - "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, - "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": "enabled", + "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": "Enabled", + "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, + "fetch_if_empty": 0, + "fieldname": "ldap_server_url", + "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": "LDAP Server Url", + "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": "organizational_unit", + "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": "Organizational Unit", + "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": "base_dn", + "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": "Base Distinguished Name (DN)", + "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": "password", + "fieldtype": "Password", + "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": "Password for Base DN", + "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": "section_break_5", + "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, + "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, + "fetch_if_empty": 0, + "fieldname": "ldap_search_string", + "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": "LDAP Search String", + "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": "ldap_first_name_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": "LDAP First Name Field", + "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": "ldap_email_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": "LDAP Email Field", + "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": "ldap_username_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": "LDAP Username Field", + "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": "ldap_security", "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": "LDAP Security", "length": 0, "no_copy": 0, @@ -325,22 +371,28 @@ "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": "Off", "description": "", + "fetch_if_empty": 0, "fieldname": "ssl_tls_mode", "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": "SSL/TLS Mode", "length": 0, "no_copy": 0, @@ -355,21 +407,27 @@ "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": "No", + "fetch_if_empty": 0, "fieldname": "require_trusted_certificate", "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": "Require Trusted Certificate", "length": 0, "no_copy": 0, @@ -384,53 +442,153 @@ "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": "local_private_key_file", + "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": "Path to private Key File", + "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, + "fetch_if_empty": 0, + "fieldname": "local_server_certificate_file", + "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": "Path to Server Certificate", + "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, + "fetch_if_empty": 0, + "fieldname": "local_ca_certs_file", + "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": "Path to CA Certs File", + "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_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 1, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2019-01-30 11:02:41.011412", - "modified_by": "Administrator", - "module": "Integrations", - "name": "LDAP Settings", - "name_case": "", - "owner": "Administrator", + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 1, + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "max_attachments": 0, + "modified": "2019-04-29 10:56:42.322696", + "modified_by": "Administrator", + "module": "Integrations", + "name": "LDAP Settings", + "name_case": "", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 0, + "role": "System Manager", + "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": 1, - "track_seen": 0 + ], + "quick_entry": 0, + "read_only": 1, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0, + "track_views": 0 } \ No newline at end of file diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py index e12a6fce05..0a4d871be8 100644 --- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py +++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py @@ -5,56 +5,90 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import cstr from frappe.model.document import Document + class LDAPSettings(Document): def validate(self): if not self.flags.ignore_mandatory: - self.validate_ldap_credentails() + if self.ldap_search_string.endswith("={0}"): + if self.enabled: + connect_to_ldap(server_url=self.ldap_server_url, + base_dn=self.base_dn, + password=self.get_password(raise_exception=False), + ssl_tls_mode=self.ssl_tls_mode, + trusted_cert=self.require_trusted_certificate, + private_key_file=self.local_private_key_file, + server_cert_file=self.local_server_certificate_file, + ca_certs_file=self.local_ca_certs_file) + else: + frappe.throw(_("LDAP Search String needs to end with a placeholder, eg sAMAccountName={0}")) - def validate_ldap_credentails(self): - try: - import ldap - conn = ldap.initialize(self.ldap_server_url) - try: - if self.ssl_tls_mode == 'StartTLS': - conn.set_option(ldap.OPT_X_TLS_DEMAND, True) - if self.require_trusted_certificate == 'Yes': - conn.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND) - conn.start_tls_s() - except: - frappe.throw(_("StartTLS is not supported")) - conn.simple_bind_s(self.base_dn, self.get_password(raise_exception=False)) - except ImportError: - msg = """ -
- {{_("Seems ldap is not installed on system.
Guidelines to install ldap dependancies and python package")}}, - {{_("Click here")}}, -
- """ - frappe.throw(msg, title=_("LDAP Not Installed")) +def get_ldap_client_settings(): + #return the settings to be used on the client side. + result = { + "enabled": False + } + settings = frappe.get_doc("LDAP Settings") - except ldap.LDAPError: - conn.unbind_s() - frappe.throw(_("Incorrect UserId or Password")) + if settings and settings.enabled: + result["enabled"] = True + result["method"] = "frappe.integrations.doctype.ldap_settings.ldap_settings.login" + return result -def get_ldap_settings(): + +def connect_to_ldap(server_url, + base_dn, + password, + ssl_tls_mode, + trusted_cert, + private_key_file, + server_cert_file, + ca_certs_file): try: - settings = frappe.get_doc("LDAP Settings") + import ldap3 + import ssl + + if trusted_cert == 'Yes': + tls_configuration = ldap3.Tls(validate=ssl.CERT_REQUIRED, + version=ssl.PROTOCOL_TLSv1) + else: + tls_configuration = ldap3.Tls(validate=ssl.CERT_NONE, + version=ssl.PROTOCOL_TLSv1) + + if private_key_file: + tls_configuration.private_key_file = private_key_file + if server_cert_file: + tls_configuration.certificate_file = server_cert_file + if ca_certs_file: + tls_configuration.ca_certs_file = ca_certs_file + + server = ldap3.Server(host=server_url, + tls=tls_configuration) + bind_type = ldap3.AUTO_BIND_TLS_BEFORE_BIND if ssl_tls_mode == "StartTLS" else True + + conn = ldap3.Connection(server=server, + user=base_dn, + password=password, + auto_bind=bind_type, + read_only=True, + raise_exceptions=True) + + return conn + + except ImportError: + msg = _("Please Install the ldap3 library via pip to use ldap functionality.") + frappe.throw(msg, title=_("LDAP Not Installed")) + except ldap3.core.exceptions.LDAPInvalidCredentialsResult: + frappe.throw(_("Invalid Credentials")) + except Exception as ex: + frappe.throw(_(str(ex))) - settings.update({ - "method": "frappe.integrations.doctype.ldap_settings.ldap_settings.login" - }) - return settings - except Exception: - # this will return blank settings - return frappe._dict() @frappe.whitelist(allow_guest=True) def login(): - #### LDAP LOGIN LOGIC ##### + # LDAP LOGIN LOGIC args = frappe.form_dict user = authenticate_ldap_user(frappe.as_unicode(args.usr), frappe.as_unicode(args.pwd)) @@ -64,64 +98,57 @@ def login(): # because of a GET request! frappe.db.commit() -def authenticate_ldap_user(user=None, password=None): - dn = None + +def authenticate_ldap_user(user=None, + password=None): + params = {} - settings = get_ldap_settings() + settings = frappe.get_doc("LDAP Settings") + if settings and settings.enabled: + conn = connect_to_ldap(server_url=settings.ldap_server_url, + base_dn=settings.base_dn, + password=settings.get_password(raise_exception=False), + ssl_tls_mode=settings.ssl_tls_mode, + trusted_cert=settings.require_trusted_certificate, + private_key_file=settings.local_private_key_file, + server_cert_file=settings.local_server_certificate_file, + ca_certs_file=settings.local_ca_certs_file) - try: - import ldap - except: - msg = """ -
- {{_("Seems ldap is not installed on system.")}}
- {{_("Click here")}}, - {{_("Guidelines to install ldap dependancies and python")}} -
- """ - frappe.throw(msg, title=_("LDAP Not Installed")) + user_filter = settings.ldap_search_string.format(user) + conn.search(search_base=settings.organizational_unit, + search_filter="({0})".format(user_filter), + attributes=[settings.ldap_email_field, + settings.ldap_username_field, + settings.ldap_first_name_field]) - conn = ldap.initialize(settings.ldap_server_url) - - try: - try: - # set TLS settings for secure connection - if settings.ssl_tls_mode == 'StartTLS': - conn.set_option(ldap.OPT_X_TLS_DEMAND, True) - if settings.require_trusted_certificate == 'Yes': - conn.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND) - conn.start_tls_s() - except: - frappe.throw(_("StartTLS is not supported")) - - # simple_bind_s is synchronous binding to server, it takes two param DN and password - conn.simple_bind_s(settings.base_dn, settings.get_password(raise_exception=False)) - - #search for surnames beginning with a - #available options for how deep a search you want. - #LDAP_SCOPE_BASE, LDAP_SCOPE_ONELEVEL,LDAP_SCOPE_SUBTREE, - result = conn.search_s(settings.organizational_unit, ldap.SCOPE_SUBTREE, - settings.ldap_search_string.format(user)) - - for dn, r in result: - dn = cstr(dn) - params["email"] = cstr(r[settings.ldap_email_field][0]) - params["username"] = cstr(r[settings.ldap_username_field][0]) - params["first_name"] = cstr(r[settings.ldap_first_name_field][0]) - - if dn: - conn.simple_bind_s(dn, frappe.as_unicode(password)) + if len(conn.entries) > 0 and conn.entries[0]: + user = conn.entries[0] + params["email"] = str(user[settings.ldap_email_field]) + params["username"] = str(user[settings.ldap_username_field]) + params["first_name"] = str(user[settings.ldap_first_name_field]) + connect_to_ldap(server_url=settings.ldap_server_url, + base_dn=user.entry_dn, + password=frappe.as_unicode(password), + ssl_tls_mode=settings.ssl_tls_mode, + trusted_cert=settings.require_trusted_certificate, + private_key_file=settings.local_private_key_file, + server_cert_file=settings.local_server_certificate_file, + ca_certs_file=settings.local_ca_certs_file + ) return create_user(params) else: frappe.throw(_("Not a valid LDAP user")) + else: + frappe.throw(_("LDAP is not enabled.")) - except ldap.LDAPError: - conn.unbind_s() - frappe.throw(_("Incorrect UserId or Password")) def create_user(params): if frappe.db.exists("User", params["email"]): - return frappe.get_doc("User", params["email"]) + user = frappe.get_doc("User", params["email"]) + user.first_name = params["first_name"] + user.username = params["username"] + user.save(ignore_permissions=True) + return user else: params.update({ @@ -135,6 +162,5 @@ def create_user(params): }) user = frappe.get_doc(params).insert(ignore_permissions=True) - frappe.db.commit() return user diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index dd0f57eb4c..992051bc45 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -66,7 +66,7 @@ login.bind_events = function() { } }); - {% if ldap_settings %} + {% if ldap_settings.enabled %} $(".btn-ldap-login").on("click", function(){ var args = {}; args.cmd = "{{ ldap_settings.method }}"; diff --git a/frappe/www/login.py b/frappe/www/login.py index dd51dabeab..f34664e1e2 100644 --- a/frappe/www/login.py +++ b/frappe/www/login.py @@ -8,7 +8,7 @@ from frappe.utils.oauth import get_oauth2_authorize_url, get_oauth_keys, login_v import json from frappe import _ from frappe.auth import LoginManager -from frappe.integrations.doctype.ldap_settings.ldap_settings import get_ldap_settings +from frappe.integrations.doctype.ldap_settings.ldap_settings import get_ldap_client_settings from frappe.utils.password import get_decrypted_password from frappe.utils.html_utils import get_icon_html @@ -39,7 +39,7 @@ def get_context(context): }) context["social_login"] = True - ldap_settings = get_ldap_settings() + ldap_settings = get_ldap_client_settings() context["ldap_settings"] = ldap_settings login_name_placeholder = [_("Email address")] From f01f36b6e583c2ce2cf43b0c0d2c5219c92f7ae4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Wed, 15 May 2019 15:41:50 +0530 Subject: [PATCH 17/60] fix: Don't show no value type fields in options for custom columns (#7497) * fix: Don't show no value type fields in options for custom columns * fix: Use filter instead of pushing in list --- frappe/public/js/frappe/views/reports/query_report.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 7baff2a25e..5e7e79312f 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -992,9 +992,11 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { change: () => { let doctype = d.get_value('doctype'); frappe.model.with_doctype(doctype, () => { - let fields = frappe.meta.get_docfields(doctype) + let options = frappe.meta.get_docfields(doctype) + .filter(frappe.model.is_value_type) .map(df => ({ label: df.label, value: df.fieldname })); - d.set_df_property('field', 'options', fields); + + d.set_df_property('field', 'options', options); }); } From b31070d427ea3995f67b523ff8c2203ee948155c Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Wed, 15 May 2019 18:00:16 +0530 Subject: [PATCH 18/60] fix: delete_doc for timeline references --- frappe/core/doctype/communication/email.py | 8 ++++---- frappe/model/delete_doc.py | 20 +++++++++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index e554e5f7cf..aea2e148b8 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -74,10 +74,10 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "has_attachment": 1 if attachments else 0 }).insert(ignore_permissions=True) - if not doctype: - # if no reference given, then send it against the communication - comm.reference_doctype = 'Communication' - comm.reference_name = comm.name + # if not doctype: + # # if no reference given, then send it against the communication + # comm.reference_doctype = 'Communication' + # comm.reference_name = comm.name contacts = get_contacts([sender, recipients, cc, bcc]) for contact_name in contacts: diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 685f295b10..ecc7956b73 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -28,6 +28,7 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa doctype = frappe.form_dict.get('dt') name = frappe.form_dict.get('dn') + print(doctype, name) names = name if isinstance(name, string_types) or isinstance(name, integer_types): names = [name] @@ -302,15 +303,24 @@ def clear_references(doctype, reference_doctype, reference_name, (reference_doctype, reference_name)) def clear_timeline_references(link_doctype, link_name): - comms = frappe.get_list("Communication", filters=[ + links = frappe.get_list("Communication", filters=[ ["Dynamic Link", "link_doctype", "=", link_doctype], ["Dynamic Link", "link_name", "=", link_name] ], fields=["name"]) - if comms: - for comm in comms: - doc = frappe.get_doc("Communication", comm.name) - doc.remove_link(link_doctype=link_doctype, link_name=link_name, autosave=True, ignore_permissions=True) + if links: + for link in links: + frappe.db.sql(""" + delete + from `tabDynamic Link` + where `tabDynamic Link`.parent='%(parent)s', + and `tabDynamic Link`.link_doctype='%(doctype)s', + and `tabDynamic Link`.link_name='%(name)s' + """,{ + "parent": link.name, + "doctype": link_doctype, + "name": link_name + }) def insert_feed(doc): from frappe.utils import get_fullname From 1a787984f496cd114ea31c8065af647120c2866c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 16 May 2019 10:16:06 +0530 Subject: [PATCH 19/60] fix: get month diff between 2 dates (#7474) (#7500) --- frappe/utils/data.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 84ea04b593..bad5407b4b 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -108,6 +108,11 @@ def add_years(date, years): def date_diff(string_ed_date, string_st_date): return (getdate(string_ed_date) - getdate(string_st_date)).days +def month_diff(string_ed_date, string_st_date): + ed_date = getdate(string_ed_date) + st_date = getdate(string_st_date) + return (ed_date.year - st_date.year) * 12 + ed_date.month - st_date.month + 1 + def time_diff(string_ed_date, string_st_date): return get_datetime(string_ed_date) - get_datetime(string_st_date) From b19f84120c7b608d5df521594a85c82fd99e5d16 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 16 May 2019 10:24:43 +0530 Subject: [PATCH 20/60] fix(link-preview): show preview only if setup and cleanup style --- frappe/core/doctype/doctype/doctype.json | 2343 ++++------------- frappe/database/mariadb/framework_mariadb.sql | 1 + .../database/postgres/framework_postgres.sql | 1 + frappe/public/js/frappe/ui/link_preview.js | 61 +- frappe/public/less/link_preview.less | 20 +- frappe/www/login.html | 6 +- 6 files changed, 505 insertions(+), 1927 deletions(-) diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index 599427f740..9aee40c2da 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -1,1889 +1,456 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 1, - "autoname": "Prompt", - "beta": 0, - "creation": "2013-02-18 13:36:19", - "custom": 0, - "description": "DocType is a Table / Form in the application.", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "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": "sb0", - "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": "", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "permlevel": 0, - "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, - "fetch_if_empty": 0, - "fieldname": "module", - "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": 1, - "label": "Module", - "length": 0, - "no_copy": 0, - "oldfieldname": "module", - "oldfieldtype": "Link", - "options": "Module Def", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "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, - "depends_on": "eval:!doc.istable", - "description": "Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended.", - "fetch_if_empty": 0, - "fieldname": "is_submittable", - "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": "Is Submittable", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "description": "Child Tables are shown as a Grid in other DocTypes", - "fetch_if_empty": 0, - "fieldname": "istable", - "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": 1, - "label": "Is Child Table", - "length": 0, - "no_copy": 0, - "oldfieldname": "istable", - "oldfieldtype": "Check", - "permlevel": 0, - "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, - "depends_on": "eval:!doc.istable", - "description": "Single Types have only one record no tables associated. Values are stored in tabSingles", - "fetch_if_empty": 0, - "fieldname": "issingle", - "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": 1, - "label": "Is Single", - "length": 0, - "no_copy": 0, - "oldfieldname": "issingle", - "oldfieldtype": "Check", - "permlevel": 0, - "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": 1, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "1", - "depends_on": "istable", - "fetch_if_empty": 0, - "fieldname": "editable_grid", - "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": "Editable Grid", - "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, - "default": "1", - "depends_on": "eval:!doc.istable && !doc.issingle", - "description": "Open a dialog with mandatory fields to create a new record quickly", - "fetch_if_empty": 0, - "fieldname": "quick_entry", - "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": "Quick Entry", - "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, - "fetch_if_empty": 0, - "fieldname": "cb01", - "fieldtype": "Column 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, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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": "1", - "depends_on": "eval:!doc.istable", - "description": "If enabled, changes to the document are tracked and shown in timeline", - "fetch_if_empty": 0, - "fieldname": "track_changes", - "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": "Track Changes", - "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, - "depends_on": "eval:!doc.istable", - "description": "If enabled, the document is marked as seen, the first time a user opens it", - "fetch_if_empty": 0, - "fieldname": "track_seen", - "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": "Track Seen", - "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, - "default": "0", - "depends_on": "eval:!doc.istable", - "description": "If enabled, document views are tracked, this can happen multiple times", - "fetch_if_empty": 0, - "fieldname": "track_views", - "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": "Track Views", - "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, - "fetch_if_empty": 0, - "fieldname": "custom", - "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": "Custom?", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "fetch_if_empty": 0, - "fieldname": "beta", - "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": "Beta", - "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, - "fetch_if_empty": 0, - "fieldname": "fields_section_break", - "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": "Fields", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "permlevel": 0, - "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, - "fetch_if_empty": 0, - "fieldname": "fields", - "fieldtype": "Table", - "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": "Fields", - "length": 0, - "no_copy": 0, - "oldfieldname": "fields", - "oldfieldtype": "Table", - "options": "DocField", - "permlevel": 0, - "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, - "fetch_if_empty": 0, - "fieldname": "sb1", - "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": "Naming", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "description": "Naming Options:\n
  1. field:[fieldname] - By Field
  2. naming_series: - By Naming Series (field called naming_series must be present
  3. Prompt - Prompt user for a name
  4. [series] - Series by prefix (separated by a dot); for example PRE.#####
  5. \n
  6. format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
", - "fetch_if_empty": 0, - "fieldname": "autoname", - "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": "Auto Name", - "length": 0, - "no_copy": 0, - "oldfieldname": "autoname", - "oldfieldtype": "Data", - "permlevel": 0, - "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, - "fetch_if_empty": 0, - "fieldname": "name_case", - "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": "Name Case", - "length": 0, - "no_copy": 0, - "oldfieldname": "name_case", - "oldfieldtype": "Select", - "options": "\nTitle Case\nUPPER CASE", - "permlevel": 0, - "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, - "depends_on": "", - "fetch_if_empty": 0, - "fieldname": "column_break_15", - "fieldtype": "Column 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, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "fetch_if_empty": 0, - "fieldname": "description", - "fieldtype": "Small Text", - "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": "Description", - "length": 0, - "no_copy": 0, - "oldfieldname": "description", - "oldfieldtype": "Text", - "permlevel": 0, - "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": 1, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "form_settings_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": "Form Settings", - "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, - "description": "Must be of type \"Attach Image\"", - "fetch_if_empty": 0, - "fieldname": "image_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": "Image Field", - "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, - "depends_on": "eval:!doc.istable", - "description": "Comments and Communications will be associated with this linked document", - "fetch_if_empty": 0, - "fieldname": "timeline_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": "Timeline Field", - "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, - "fetch_if_empty": 0, - "fieldname": "max_attachments", - "fieldtype": "Int", - "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": "Max Attachments", - "length": 0, - "no_copy": 0, - "oldfieldname": "max_attachments", - "oldfieldtype": "Int", - "permlevel": 0, - "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, - "fetch_if_empty": 0, - "fieldname": "column_break_23", - "fieldtype": "Column 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, - "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, - "fetch_if_empty": 0, - "fieldname": "hide_toolbar", - "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": "Hide Sidebar and Menu", - "length": 0, - "no_copy": 0, - "oldfieldname": "hide_toolbar", - "oldfieldtype": "Check", - "permlevel": 0, - "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, - "description": "", - "fetch_if_empty": 0, - "fieldname": "allow_copy", - "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": "Hide Copy", - "length": 0, - "no_copy": 0, - "oldfieldname": "allow_copy", - "oldfieldtype": "Check", - "permlevel": 0, - "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, - "fetch_if_empty": 0, - "fieldname": "allow_rename", - "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": "Allow Rename", - "length": 0, - "no_copy": 0, - "oldfieldname": "allow_rename", - "oldfieldtype": "Check", - "permlevel": 0, - "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, - "description": "", - "fetch_if_empty": 0, - "fieldname": "allow_import", - "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": "Allow Import (via Data Import Tool)", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "fetch_if_empty": 0, - "fieldname": "allow_events_in_timeline", - "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": "Allow events in timeline", - "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": 1, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "view_settings", - "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": "View Settings", - "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, - "depends_on": "eval:!doc.istable", - "description": "", - "fetch_if_empty": 0, - "fieldname": "title_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": "Title Field", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "depends_on": "eval:!doc.istable", - "fetch_if_empty": 0, - "fieldname": "search_fields", - "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": "Search Fields", - "length": 0, - "no_copy": 0, - "oldfieldname": "search_fields", - "oldfieldtype": "Data", - "permlevel": 0, - "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, - "fetch_if_empty": 0, - "fieldname": "default_print_format", - "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": "Default Print Format", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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": "modified", - "depends_on": "eval:!doc.istable", - "description": "", - "fetch_if_empty": 0, - "fieldname": "sort_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": "Default Sort Field", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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": "DESC", - "depends_on": "eval:!doc.istable", - "fetch_if_empty": 0, - "fieldname": "sort_order", - "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": "Default Sort Order", - "length": 0, - "no_copy": 0, - "options": "ASC\nDESC", - "permlevel": 0, - "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, - "fetch_if_empty": 0, - "fieldname": "column_break_29", - "fieldtype": "Column 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, - "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, - "description": "", - "fetch_if_empty": 0, - "fieldname": "document_type", - "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": "Show in Module Section", - "length": 0, - "no_copy": 0, - "oldfieldname": "document_type", - "oldfieldtype": "Select", - "options": "\nDocument\nSetup\nSystem\nOther", - "permlevel": 0, - "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, - "fetch_if_empty": 0, - "fieldname": "icon", - "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": "Icon", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "fetch_if_empty": 0, - "fieldname": "color", - "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": "Color", - "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, - "fetch_if_empty": 0, - "fieldname": "show_name_in_global_search", - "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": "Make \"name\" searchable in Global Search", - "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, - "depends_on": "eval:!doc.istable", - "fetch_if_empty": 0, - "fieldname": "sb2", - "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": "Permission Rules", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "depends_on": "", - "fetch_if_empty": 0, - "fieldname": "permissions", - "fieldtype": "Table", - "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": "Permissions", - "length": 0, - "no_copy": 0, - "oldfieldname": "permissions", - "oldfieldtype": "Table", - "options": "DocPerm", - "permlevel": 0, - "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, - "fetch_if_empty": 0, - "fieldname": "restrict_to_domain", - "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": "Restrict To Domain", - "length": 0, - "no_copy": 0, - "options": "Domain", - "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, - "fetch_if_empty": 0, - "fieldname": "read_only", - "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": "User Cannot Search", - "length": 0, - "no_copy": 0, - "oldfieldname": "read_only", - "oldfieldtype": "Check", - "permlevel": 0, - "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, - "fetch_if_empty": 0, - "fieldname": "in_create", - "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": "User Cannot Create", - "length": 0, - "no_copy": 0, - "oldfieldname": "in_create", - "oldfieldtype": "Check", - "permlevel": 0, - "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, - "description": "", - "fetch_if_empty": 0, - "fieldname": "web_view", - "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": "Web View", - "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, - "default": "0", - "fetch_if_empty": 0, - "fieldname": "has_web_view", - "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": "Has Web View", - "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, - "default": "0", - "depends_on": "has_web_view", - "fetch_if_empty": 0, - "fieldname": "allow_guest_to_view", - "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": "Allow Guest to View", - "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, - "depends_on": "has_web_view", - "fetch_if_empty": 0, - "fieldname": "route", - "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": "Route", - "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, - "default": "", - "depends_on": "has_web_view", - "fetch_if_empty": 0, - "fieldname": "is_published_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": "Is Published Field", - "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": 1, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "advanced", - "fieldtype": "Section Break", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Advanced", - "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, - "default": "InnoDB", - "depends_on": "eval:!doc.issingle", - "fetch_if_empty": 0, - "fieldname": "engine", - "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": "Database Engine", - "length": 0, - "no_copy": 0, - "options": "InnoDB\nMyISAM", - "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_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-bolt", - "idx": 6, - "image_field": "", - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2019-03-22 00:02:14.963400", - "modified_by": "Administrator", - "module": "Core", - "name": "DocType", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Administrator", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "module", - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 - } \ No newline at end of file + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2013-02-18 13:36:19", + "description": "DocType is a Table / Form in the application.", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "sb0", + "module", + "is_submittable", + "istable", + "issingle", + "editable_grid", + "quick_entry", + "cb01", + "track_changes", + "track_seen", + "track_views", + "custom", + "beta", + "fields_section_break", + "fields", + "sb1", + "autoname", + "name_case", + "column_break_15", + "description", + "form_settings_section", + "image_field", + "timeline_field", + "max_attachments", + "column_break_23", + "hide_toolbar", + "allow_copy", + "allow_rename", + "allow_import", + "allow_events_in_timeline", + "view_settings", + "title_field", + "search_fields", + "default_print_format", + "sort_field", + "sort_order", + "column_break_29", + "document_type", + "icon", + "color", + "show_preview_popup", + "show_name_in_global_search", + "sb2", + "permissions", + "restrict_to_domain", + "read_only", + "in_create", + "web_view", + "has_web_view", + "allow_guest_to_view", + "route", + "is_published_field", + "advanced", + "engine" + ], + "fields": [ + { + "fieldname": "sb0", + "fieldtype": "Section Break", + "oldfieldtype": "Section Break" + }, + { + "fieldname": "module", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Module", + "oldfieldname": "module", + "oldfieldtype": "Link", + "options": "Module Def", + "reqd": 1, + "search_index": 1 + }, + { + "depends_on": "eval:!doc.istable", + "description": "Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended.", + "fieldname": "is_submittable", + "fieldtype": "Check", + "label": "Is Submittable" + }, + { + "description": "Child Tables are shown as a Grid in other DocTypes", + "fieldname": "istable", + "fieldtype": "Check", + "in_standard_filter": 1, + "label": "Is Child Table", + "oldfieldname": "istable", + "oldfieldtype": "Check" + }, + { + "depends_on": "eval:!doc.istable", + "description": "Single Types have only one record no tables associated. Values are stored in tabSingles", + "fieldname": "issingle", + "fieldtype": "Check", + "in_standard_filter": 1, + "label": "Is Single", + "oldfieldname": "issingle", + "oldfieldtype": "Check", + "set_only_once": 1 + }, + { + "default": "1", + "depends_on": "istable", + "fieldname": "editable_grid", + "fieldtype": "Check", + "label": "Editable Grid" + }, + { + "default": "1", + "depends_on": "eval:!doc.istable && !doc.issingle", + "description": "Open a dialog with mandatory fields to create a new record quickly", + "fieldname": "quick_entry", + "fieldtype": "Check", + "label": "Quick Entry" + }, + { + "fieldname": "cb01", + "fieldtype": "Column Break" + }, + { + "default": "1", + "depends_on": "eval:!doc.istable", + "description": "If enabled, changes to the document are tracked and shown in timeline", + "fieldname": "track_changes", + "fieldtype": "Check", + "label": "Track Changes" + }, + { + "depends_on": "eval:!doc.istable", + "description": "If enabled, the document is marked as seen, the first time a user opens it", + "fieldname": "track_seen", + "fieldtype": "Check", + "label": "Track Seen" + }, + { + "default": "0", + "depends_on": "eval:!doc.istable", + "description": "If enabled, document views are tracked, this can happen multiple times", + "fieldname": "track_views", + "fieldtype": "Check", + "label": "Track Views" + }, + { + "fieldname": "custom", + "fieldtype": "Check", + "label": "Custom?" + }, + { + "fieldname": "beta", + "fieldtype": "Check", + "label": "Beta" + }, + { + "fieldname": "fields_section_break", + "fieldtype": "Section Break", + "label": "Fields", + "oldfieldtype": "Section Break" + }, + { + "fieldname": "fields", + "fieldtype": "Table", + "label": "Fields", + "oldfieldname": "fields", + "oldfieldtype": "Table", + "options": "DocField" + }, + { + "fieldname": "sb1", + "fieldtype": "Section Break", + "label": "Naming" + }, + { + "description": "Naming Options:\n
  1. field:[fieldname] - By Field
  2. naming_series: - By Naming Series (field called naming_series must be present
  3. Prompt - Prompt user for a name
  4. [series] - Series by prefix (separated by a dot); for example PRE.#####
  5. \n
  6. format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
", + "fieldname": "autoname", + "fieldtype": "Data", + "label": "Auto Name", + "oldfieldname": "autoname", + "oldfieldtype": "Data" + }, + { + "fieldname": "name_case", + "fieldtype": "Select", + "label": "Name Case", + "oldfieldname": "name_case", + "oldfieldtype": "Select", + "options": "\nTitle Case\nUPPER CASE" + }, + { + "fieldname": "column_break_15", + "fieldtype": "Column Break" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Text" + }, + { + "collapsible": 1, + "fieldname": "form_settings_section", + "fieldtype": "Section Break", + "label": "Form Settings" + }, + { + "description": "Must be of type \"Attach Image\"", + "fieldname": "image_field", + "fieldtype": "Data", + "label": "Image Field" + }, + { + "depends_on": "eval:!doc.istable", + "description": "Comments and Communications will be associated with this linked document", + "fieldname": "timeline_field", + "fieldtype": "Data", + "label": "Timeline Field" + }, + { + "fieldname": "max_attachments", + "fieldtype": "Int", + "label": "Max Attachments", + "oldfieldname": "max_attachments", + "oldfieldtype": "Int" + }, + { + "fieldname": "column_break_23", + "fieldtype": "Column Break" + }, + { + "fieldname": "hide_toolbar", + "fieldtype": "Check", + "label": "Hide Sidebar and Menu", + "oldfieldname": "hide_toolbar", + "oldfieldtype": "Check" + }, + { + "fieldname": "allow_copy", + "fieldtype": "Check", + "label": "Hide Copy", + "oldfieldname": "allow_copy", + "oldfieldtype": "Check" + }, + { + "fieldname": "allow_rename", + "fieldtype": "Check", + "label": "Allow Rename", + "oldfieldname": "allow_rename", + "oldfieldtype": "Check" + }, + { + "fieldname": "allow_import", + "fieldtype": "Check", + "label": "Allow Import (via Data Import Tool)" + }, + { + "fieldname": "allow_events_in_timeline", + "fieldtype": "Check", + "label": "Allow events in timeline" + }, + { + "collapsible": 1, + "fieldname": "view_settings", + "fieldtype": "Section Break", + "label": "View Settings" + }, + { + "depends_on": "eval:!doc.istable", + "fieldname": "title_field", + "fieldtype": "Data", + "label": "Title Field" + }, + { + "depends_on": "eval:!doc.istable", + "fieldname": "search_fields", + "fieldtype": "Data", + "label": "Search Fields", + "oldfieldname": "search_fields", + "oldfieldtype": "Data" + }, + { + "fieldname": "default_print_format", + "fieldtype": "Data", + "label": "Default Print Format" + }, + { + "default": "modified", + "depends_on": "eval:!doc.istable", + "fieldname": "sort_field", + "fieldtype": "Data", + "label": "Default Sort Field" + }, + { + "default": "DESC", + "depends_on": "eval:!doc.istable", + "fieldname": "sort_order", + "fieldtype": "Select", + "label": "Default Sort Order", + "options": "ASC\nDESC" + }, + { + "fieldname": "column_break_29", + "fieldtype": "Column Break" + }, + { + "fieldname": "document_type", + "fieldtype": "Select", + "label": "Show in Module Section", + "oldfieldname": "document_type", + "oldfieldtype": "Select", + "options": "\nDocument\nSetup\nSystem\nOther" + }, + { + "fieldname": "icon", + "fieldtype": "Data", + "label": "Icon" + }, + { + "fieldname": "color", + "fieldtype": "Data", + "label": "Color" + }, + { + "fieldname": "show_name_in_global_search", + "fieldtype": "Check", + "label": "Make \"name\" searchable in Global Search" + }, + { + "depends_on": "eval:!doc.istable", + "fieldname": "sb2", + "fieldtype": "Section Break", + "label": "Permission Rules" + }, + { + "fieldname": "permissions", + "fieldtype": "Table", + "label": "Permissions", + "oldfieldname": "permissions", + "oldfieldtype": "Table", + "options": "DocPerm" + }, + { + "fieldname": "restrict_to_domain", + "fieldtype": "Link", + "label": "Restrict To Domain", + "options": "Domain" + }, + { + "fieldname": "read_only", + "fieldtype": "Check", + "label": "User Cannot Search", + "oldfieldname": "read_only", + "oldfieldtype": "Check" + }, + { + "fieldname": "in_create", + "fieldtype": "Check", + "label": "User Cannot Create", + "oldfieldname": "in_create", + "oldfieldtype": "Check" + }, + { + "fieldname": "web_view", + "fieldtype": "Section Break", + "label": "Web View" + }, + { + "default": "0", + "fieldname": "has_web_view", + "fieldtype": "Check", + "label": "Has Web View" + }, + { + "default": "0", + "depends_on": "has_web_view", + "fieldname": "allow_guest_to_view", + "fieldtype": "Check", + "label": "Allow Guest to View" + }, + { + "depends_on": "has_web_view", + "fieldname": "route", + "fieldtype": "Data", + "label": "Route" + }, + { + "depends_on": "has_web_view", + "fieldname": "is_published_field", + "fieldtype": "Data", + "label": "Is Published Field" + }, + { + "collapsible": 1, + "fieldname": "advanced", + "fieldtype": "Section Break", + "hidden": 1, + "label": "Advanced" + }, + { + "default": "InnoDB", + "depends_on": "eval:!doc.issingle", + "fieldname": "engine", + "fieldtype": "Select", + "label": "Database Engine", + "options": "InnoDB\nMyISAM" + }, + { + "fieldname": "show_preview_popup", + "fieldtype": "Check", + "label": "Show Preview Popup" + } + ], + "icon": "fa fa-bolt", + "idx": 6, + "modified": "2019-05-16 09:18:29.373576", + "modified_by": "Administrator", + "module": "Core", + "name": "DocType", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "write": 1 + } + ], + "search_fields": "module", + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql index 48c70afbbc..ad4cd7d5ef 100644 --- a/frappe/database/mariadb/framework_mariadb.sql +++ b/frappe/database/mariadb/framework_mariadb.sql @@ -37,6 +37,7 @@ CREATE TABLE `tabDocField` ( `unique` int(1) NOT NULL DEFAULT 0, `no_copy` int(1) NOT NULL DEFAULT 0, `allow_on_submit` int(1) NOT NULL DEFAULT 0, + `show_preview_popup` int(1) NOT NULL DEFAULT 0, `trigger` varchar(255) DEFAULT NULL, `collapsible_depends_on` text, `depends_on` text, diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql index 2ff024555d..756917ca97 100644 --- a/frappe/database/postgres/framework_postgres.sql +++ b/frappe/database/postgres/framework_postgres.sql @@ -37,6 +37,7 @@ CREATE TABLE "tabDocField" ( "unique" smallint NOT NULL DEFAULT 0, "no_copy" smallint NOT NULL DEFAULT 0, "allow_on_submit" smallint NOT NULL DEFAULT 0, + "show_preview_popup" smallint NOT NULL DEFAULT 0, "trigger" varchar(255) DEFAULT NULL, "collapsible_depends_on" text, "depends_on" text, diff --git a/frappe/public/js/frappe/ui/link_preview.js b/frappe/public/js/frappe/ui/link_preview.js index c0e2d162a6..538b187d9f 100644 --- a/frappe/public/js/frappe/ui/link_preview.js +++ b/frappe/public/js/frappe/ui/link_preview.js @@ -46,6 +46,9 @@ frappe.ui.LinkPreview = class { }); } else { this.popover_timeout = setTimeout(() => { + if (this.element.is(':focus')) { + return; + } this.popover.show(); }, 1000); } @@ -53,6 +56,10 @@ frappe.ui.LinkPreview = class { } create_popover(e, preview_fields) { + if (this.element.is(':focus')) { + return; + } + this.get_preview_fields_value(preview_fields).then((preview_data)=> { if(preview_data) { if(this.popover_timeout) { @@ -93,12 +100,6 @@ frappe.ui.LinkPreview = class { this.clear_all_popovers(); }); - $(document.body).on('mousemove', () => { - if (!this.link_hovered) { - this.clear_all_popovers(); - } - }); - $(window).on('hashchange', () => { this.clear_all_popovers(); }); @@ -113,7 +114,15 @@ frappe.ui.LinkPreview = class { let dt = this.doctype; let fields = []; frappe.model.with_doctype(dt, () => { - let meta_fields = frappe.get_meta(dt).fields; + let meta = frappe.get_meta(dt); + let meta_fields = meta.fields; + + if (!meta.show_preview_popup) { + // no preview + resolve([]); + return; + } + meta_fields.filter((field) => { // build list of fields to fetch if(field.in_preview) { @@ -171,41 +180,45 @@ frappe.ui.LinkPreview = class { } let image_html = ''; - let title_html = ''; - let content_html = ``; + let id_html = ''; + let content_html = ''; + let meta = frappe.get_meta(this.doctype); + let title = preview_data[meta.title_field]; - if(preview_data['image']) { - let image_url = encodeURI(preview_data['image']); + if(preview_data[meta.image_field]) { + let image_url = encodeURI(preview_data[meta.image_field]); image_html += `
`; } - if(preview_data['title']) { - title_html+= `${preview_data['title']}`; + + + if(title && title != preview_data.name) { + id_html+= `${preview_data.name}`; } Object.keys(preview_data).forEach(key => { - if(key!='image' && key!='name') { + if(key!=meta.image_field && key!='name' && key!=meta.title_field) { let value = this.truncate_value(preview_data[key]); let label = this.truncate_value(frappe.meta.get_label(this.doctype, key)); content_html += ` - - - - +
+
${label}
+
${value}
+
`; } }); - content_html+=`
${label} ${value}
`; + + content_html = `
${content_html}
`; let popover_content = `
${image_html}
- ${preview_data['name']} - ${title_html} - ${this.doctype} + ${title} + ${this.doctype} ${id_html}
@@ -218,8 +231,8 @@ frappe.ui.LinkPreview = class { } truncate_value(value) { - if (value.length > 100) { - value = value.slice(0,100) + '...'; + if (value.length > 280) { + value = value.slice(0,280) + '...'; } return value; } diff --git a/frappe/public/less/link_preview.less b/frappe/public/less/link_preview.less index 2a3ccce5b3..9e8ed30e57 100644 --- a/frappe/public/less/link_preview.less +++ b/frappe/public/less/link_preview.less @@ -4,8 +4,7 @@ .popover-content { padding: 0; .preview-popover-header { - background: #f7fafc; - padding: 10px; + padding: 15px; .preview-header { display: inline-block; @@ -34,19 +33,16 @@ } .preview-table { - margin: 10px; + padding: 15px; + padding-bottom: 5px; max-width: 330px; min-width: 200px; - } - .preview-field { - td { - padding: 3px; - vertical-align: top; - } - - .field-name { - width: 39%; + .preview-field { + padding-bottom: 10px; + .preview-label { + padding-bottom: 4px; + } } } } diff --git a/frappe/www/login.html b/frappe/www/login.html index 9d132cb718..d621ab0fbb 100644 --- a/frappe/www/login.html +++ b/frappe/www/login.html @@ -24,9 +24,9 @@
- - -
+ + + From d075c3b748d86f12fd368bec23ce2e56e18af28b Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Thu, 16 May 2019 13:26:35 +0530 Subject: [PATCH 21/60] fix: link error while deleting linked doc --- frappe/model/delete_doc.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index ecc7956b73..86f72e7416 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -83,6 +83,7 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa doc.flags.in_delete = True doc.run_method('on_change') + clear_timeline_references(doc.doctype, doc.name) frappe.enqueue('frappe.model.delete_doc.delete_dynamic_links', doctype=doc.doctype, name=doc.name, is_async=False if frappe.flags.in_test else True) @@ -310,17 +311,8 @@ def clear_timeline_references(link_doctype, link_name): if links: for link in links: - frappe.db.sql(""" - delete - from `tabDynamic Link` - where `tabDynamic Link`.parent='%(parent)s', - and `tabDynamic Link`.link_doctype='%(doctype)s', - and `tabDynamic Link`.link_name='%(name)s' - """,{ - "parent": link.name, - "doctype": link_doctype, - "name": link_name - }) + doc = frappe.get_doc("Communication", link.name) + doc.remove_link(link_doctype=link_doctype, link_name=link_name, autosave=True) def insert_feed(doc): from frappe.utils import get_fullname From e5b97de25538fd23d38e13bee588e63dea697ba2 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 16 May 2019 15:00:14 +0530 Subject: [PATCH 22/60] fix(minor): default check value to 0 and validate --- frappe/core/doctype/doctype/doctype.json | 3 ++- frappe/core/doctype/doctype/doctype.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index 9aee40c2da..24a7a4c287 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -413,6 +413,7 @@ "options": "InnoDB\nMyISAM" }, { + "default": "0", "fieldname": "show_preview_popup", "fieldtype": "Check", "label": "Show Preview Popup" @@ -420,7 +421,7 @@ ], "icon": "fa fa-bolt", "idx": 6, - "modified": "2019-05-16 09:18:29.373576", + "modified": "2019-05-16 14:58:33.405381", "modified_by": "Administrator", "module": "Core", "name": "DocType", diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 5db025f9fb..7efdba3237 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -616,7 +616,9 @@ def validate_fields(meta): frappe.throw(_("Options 'Dynamic Link' type of field must point to another Link Field with options as 'DocType'")) def check_illegal_default(d): - if d.fieldtype == "Check" and d.default and d.default not in ('0', '1'): + if d.fieldtype == "Check" and not d.default: + d.default = '0' + if d.fieldtype == "Check" and d.default not in ('0', '1'): frappe.throw(_("Default for 'Check' type of field must be either '0' or '1'")) if d.fieldtype == "Select" and d.default and (d.default not in d.options.split("\n")): frappe.throw(_("Default for {0} must be an option").format(d.fieldname)) From e2d3b863745fb6264de4b9d6446104735a1dccac Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 16 May 2019 15:20:31 +0530 Subject: [PATCH 23/60] fix(null-handling): for doctype, doctype, ignore null values --- frappe/model/base_document.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 8b03a21a2b..cea939abdc 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -197,7 +197,7 @@ class BaseDocument(object): return value - def get_valid_dict(self, sanitize=True, convert_dates_to_str=False): + def get_valid_dict(self, sanitize=True, convert_dates_to_str=False, ignore_nulls = False): d = frappe._dict() for fieldname in self.meta.get_valid_columns(): d[fieldname] = self.get(fieldname) @@ -234,6 +234,9 @@ class BaseDocument(object): if convert_dates_to_str and isinstance(d[fieldname], (datetime.datetime, datetime.time, datetime.timedelta)): d[fieldname] = str(d[fieldname]) + if d[fieldname] == None and ignore_nulls: + del d[fieldname] + return d def init_valid_columns(self): @@ -306,7 +309,8 @@ class BaseDocument(object): self.creation = self.modified = now() self.created_by = self.modified_by = frappe.session.user - d = self.get_valid_dict(convert_dates_to_str=True) + # if doctype is "DocType", don't insert null values as we don't know who is valid yet + d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls=self.doctype in ('DocType', 'DocField', 'DocPerm')) columns = list(d) try: From 0284b1a9655a80e28e3a59b8a1a847a74659eb87 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 16 May 2019 15:37:23 +0530 Subject: [PATCH 24/60] fix(null-handling): for doctype, doctype, ignore null values --- frappe/core/doctype/comment/test_comment.py | 4 ++-- frappe/model/base_document.py | 4 ++-- frappe/templates/includes/comments/comments.py | 15 ++++++++------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/frappe/core/doctype/comment/test_comment.py b/frappe/core/doctype/comment/test_comment.py index 2f6583b7ba..2adc5eb899 100644 --- a/frappe/core/doctype/comment/test_comment.py +++ b/frappe/core/doctype/comment/test_comment.py @@ -48,10 +48,10 @@ class TestComment(unittest.TestCase): add_comment('pleez vizits my site http://mysite.com', 'test@test.com', 'bad commentor', 'Blog Post', test_blog.name, test_blog.route) - self.assertEqual(frappe.get_all('Comment', fields = ['*'], filters = dict( + self.assertEqual(len(frappe.get_all('Comment', fields = ['*'], filters = dict( reference_doctype = test_blog.doctype, reference_name = test_blog.name - ))[0].published, 0) + ))), 0) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index cea939abdc..ae4bc07b1a 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -310,7 +310,7 @@ class BaseDocument(object): self.created_by = self.modified_by = frappe.session.user # if doctype is "DocType", don't insert null values as we don't know who is valid yet - d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls=self.doctype in ('DocType', 'DocField', 'DocPerm')) + d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in ('DocType', 'DocField', 'DocPerm')) columns = list(d) try: @@ -345,7 +345,7 @@ class BaseDocument(object): self.db_insert() return - d = self.get_valid_dict(convert_dates_to_str=True) + d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in ('DocType', 'DocField', 'DocPerm')) # don't update name, as case might've been changed name = d['name'] diff --git a/frappe/templates/includes/comments/comments.py b/frappe/templates/includes/comments/comments.py index 52abb60b4d..8874f59d86 100644 --- a/frappe/templates/includes/comments/comments.py +++ b/frappe/templates/includes/comments/comments.py @@ -16,16 +16,17 @@ def add_comment(comment, comment_email, comment_by, reference_doctype, reference frappe.msgprint(_('Comment Should be atleast 10 characters')) return '' + blacklist = ['http://', 'https://', '@gmail.com'] + + if any([b in comment.content for b in blacklist]): + frappe.msgprint(_('Comments cannot have links or email addresses')) + return '' + comment = doc.add_comment( text = comment, comment_email = comment_email, - comment_by = comment_by) - - blacklist = ['http://', 'https://', '@gmail.com'] - - if not any([b in comment.content for b in blacklist]): - # probably not spam! - comment.db_set('published', 1) + comment_by = comment_by, + published = 1) # since comments are embedded in the page, clear the web cache if route: From e4f70da7cdf0a1b6e0063604841fe2e76fc61ff0 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Thu, 16 May 2019 17:37:37 +0530 Subject: [PATCH 25/60] fix: Report Print format for indented rows (#7505) --- .../js/frappe/views/reports/print_grid.html | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/frappe/public/js/frappe/views/reports/print_grid.html b/frappe/public/js/frappe/views/reports/print_grid.html index 12854e74f3..e99600c2e6 100644 --- a/frappe/public/js/frappe/views/reports/print_grid.html +++ b/frappe/public/js/frappe/views/reports/print_grid.html @@ -31,15 +31,17 @@ {% var value = col.fieldname ? row[col.fieldname] : row[col.id]; %} - {{ - col.formatter - ? col.formatter(row._index, col._index, value, col, row, true) - : col.format - ? col.format(value, row, col, data) - : col.docfield - ? frappe.format(value, col.docfield) - : value - }} + + {{ + col.formatter + ? col.formatter(row._index, col._index, value, col, row, true) + : col.format + ? col.format(value, row, col, data) + : col.docfield + ? frappe.format(value, col.docfield) + : value + }} + {% endif %} {% endfor %} From 005c1a8de178b20f8efc0187049640430a2123fa Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Thu, 16 May 2019 17:40:58 +0530 Subject: [PATCH 26/60] fix: use frappe sql for query --- frappe/model/delete_doc.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 86f72e7416..88c5101617 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -83,7 +83,6 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa doc.flags.in_delete = True doc.run_method('on_change') - clear_timeline_references(doc.doctype, doc.name) frappe.enqueue('frappe.model.delete_doc.delete_dynamic_links', doctype=doc.doctype, name=doc.name, is_async=False if frappe.flags.in_test else True) @@ -304,15 +303,8 @@ def clear_references(doctype, reference_doctype, reference_name, (reference_doctype, reference_name)) def clear_timeline_references(link_doctype, link_name): - links = frappe.get_list("Communication", filters=[ - ["Dynamic Link", "link_doctype", "=", link_doctype], - ["Dynamic Link", "link_name", "=", link_name] - ], fields=["name"]) - - if links: - for link in links: - doc = frappe.get_doc("Communication", link.name) - doc.remove_link(link_doctype=link_doctype, link_name=link_name, autosave=True) + frappe.db.sql('''delete from `tabDynamic Link` + where `tabDynamic Link`.link_doctype={0} and `tabDynamic Link`.link_name={1}'''.format(link_doctype, link_name)) # nosec def insert_feed(doc): from frappe.utils import get_fullname From 29c0d6f16a855300cd540d6260da3029ac7e43d2 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 16 May 2019 17:58:27 +0530 Subject: [PATCH 27/60] fix(minor): comments.py --- frappe/templates/includes/comments/comments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/templates/includes/comments/comments.py b/frappe/templates/includes/comments/comments.py index 8874f59d86..6a2cc1dfaa 100644 --- a/frappe/templates/includes/comments/comments.py +++ b/frappe/templates/includes/comments/comments.py @@ -18,7 +18,7 @@ def add_comment(comment, comment_email, comment_by, reference_doctype, reference blacklist = ['http://', 'https://', '@gmail.com'] - if any([b in comment.content for b in blacklist]): + if any([b in comment for b in blacklist]): frappe.msgprint(_('Comments cannot have links or email addresses')) return '' From 43bc6ca8d14ae4e99a8b688648cab5be41938ec7 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Thu, 16 May 2019 18:36:10 +0530 Subject: [PATCH 28/60] fix: missing quotes in where clause --- frappe/model/delete_doc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 88c5101617..75365ce9ba 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -303,8 +303,8 @@ def clear_references(doctype, reference_doctype, reference_name, (reference_doctype, reference_name)) def clear_timeline_references(link_doctype, link_name): - frappe.db.sql('''delete from `tabDynamic Link` - where `tabDynamic Link`.link_doctype={0} and `tabDynamic Link`.link_name={1}'''.format(link_doctype, link_name)) # nosec + frappe.db.sql("""delete from `tabDynamic Link` + where `tabDynamic Link`.link_doctype='{0}' and `tabDynamic Link`.link_name='{1}'""".format(link_doctype, link_name)) # nosec def insert_feed(doc): from frappe.utils import get_fullname From c89185aed8d07db98b3dacd9003da04f26de51e9 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 16 May 2019 18:45:41 +0530 Subject: [PATCH 29/60] fix(minor): comments.py --- frappe/templates/includes/comments/comments.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frappe/templates/includes/comments/comments.py b/frappe/templates/includes/comments/comments.py index 6a2cc1dfaa..cf2436da15 100644 --- a/frappe/templates/includes/comments/comments.py +++ b/frappe/templates/includes/comments/comments.py @@ -25,8 +25,9 @@ def add_comment(comment, comment_email, comment_by, reference_doctype, reference comment = doc.add_comment( text = comment, comment_email = comment_email, - comment_by = comment_by, - published = 1) + comment_by = comment_by) + + comment.db_set('published', 1) # since comments are embedded in the page, clear the web cache if route: From aa98dd28a6413d6d49a966d9b96d58967e3325b6 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Thu, 16 May 2019 20:04:49 +0530 Subject: [PATCH 30/60] test: communication tests --- .../core/doctype/communication/test_communication.py | 7 +++++++ frappe/core/doctype/dynamic_link/dynamic_link.json | 11 +++-------- frappe/model/delete_doc.py | 3 +-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/frappe/core/doctype/communication/test_communication.py b/frappe/core/doctype/communication/test_communication.py index 53b0981af7..3b08aaeddb 100644 --- a/frappe/core/doctype/communication/test_communication.py +++ b/frappe/core/doctype/communication/test_communication.py @@ -58,6 +58,8 @@ class TestCommunication(unittest.TestCase): "reference_name": a.name }).insert() + b.add_link(link_doctype="Communication", link_name=a.name, autosave=True) + c = frappe.get_doc({ "doctype": "Communication", "communication_type": "Communication", @@ -66,8 +68,13 @@ class TestCommunication(unittest.TestCase): "reference_name": b.name }).insert() + c.add_link(link_doctype="Communication", link_name=b.name, autosave=True) + a = frappe.get_doc("Communication", a.name) a.reference_doctype = "Communication" a.reference_name = c.name self.assertRaises(frappe.CircularLinkingError, a.save) + + a.add_link(link_doctype="Communication", link_name=c.name) + self.assertRaises(frappe.CircularLinkingError, c.save) diff --git a/frappe/core/doctype/dynamic_link/dynamic_link.json b/frappe/core/doctype/dynamic_link/dynamic_link.json index a8b871855c..abc47df100 100644 --- a/frappe/core/doctype/dynamic_link/dynamic_link.json +++ b/frappe/core/doctype/dynamic_link/dynamic_link.json @@ -5,9 +5,8 @@ "engine": "InnoDB", "field_order": [ "link_doctype", - "link_title", - "cb_00", - "link_name" + "link_name", + "link_title" ], "fields": [ { @@ -32,14 +31,10 @@ "in_list_view": 1, "label": "Link Title", "read_only": 1 - }, - { - "fieldname": "cb_00", - "fieldtype": "Column Break" } ], "istable": 1, - "modified": "2019-05-14 17:36:38.391805", + "modified": "2019-05-16 19:54:31.400026", "modified_by": "Administrator", "module": "Core", "name": "Dynamic Link", diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 75365ce9ba..73c6719d2b 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -28,7 +28,6 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa doctype = frappe.form_dict.get('dt') name = frappe.form_dict.get('dn') - print(doctype, name) names = name if isinstance(name, string_types) or isinstance(name, integer_types): names = [name] @@ -279,9 +278,9 @@ def delete_dynamic_links(doctype, name): delete_references('Document Follow', doctype, name, 'ref_doctype', 'ref_docname') # unlink communications + clear_timeline_references(doctype, name) clear_references('Communication', doctype, name) clear_references('Communication', doctype, name, 'link_doctype', 'link_name') - clear_timeline_references(doctype, name) clear_references('Activity Log', doctype, name) clear_references('Activity Log', doctype, name, 'timeline_doctype', 'timeline_name') From 0d9b7048d94c97c577cf0f7c0f7508cb16bb15a0 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Fri, 17 May 2019 00:06:38 +0530 Subject: [PATCH 31/60] fix: tests for communication --- .../doctype/communication/test_communication.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/frappe/core/doctype/communication/test_communication.py b/frappe/core/doctype/communication/test_communication.py index 3b08aaeddb..715afe2cb3 100644 --- a/frappe/core/doctype/communication/test_communication.py +++ b/frappe/core/doctype/communication/test_communication.py @@ -48,7 +48,7 @@ class TestCommunication(unittest.TestCase): "doctype": "Communication", "communication_type": "Communication", "content": "This was created to test circular linking: Communication A", - }).insert() + }).insert(ignore_permissions=True) b = frappe.get_doc({ "doctype": "Communication", @@ -56,9 +56,7 @@ class TestCommunication(unittest.TestCase): "content": "This was created to test circular linking: Communication B", "reference_doctype": "Communication", "reference_name": a.name - }).insert() - - b.add_link(link_doctype="Communication", link_name=a.name, autosave=True) + }).insert(ignore_permissions=True) c = frappe.get_doc({ "doctype": "Communication", @@ -66,15 +64,10 @@ class TestCommunication(unittest.TestCase): "content": "This was created to test circular linking: Communication C", "reference_doctype": "Communication", "reference_name": b.name - }).insert() - - c.add_link(link_doctype="Communication", link_name=b.name, autosave=True) + }).insert(ignore_permissions=True) a = frappe.get_doc("Communication", a.name) a.reference_doctype = "Communication" a.reference_name = c.name self.assertRaises(frappe.CircularLinkingError, a.save) - - a.add_link(link_doctype="Communication", link_name=c.name) - self.assertRaises(frappe.CircularLinkingError, c.save) From 08fb05233467e665e1e25e98db36a3ad41e7ab27 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Fri, 17 May 2019 14:21:37 +0530 Subject: [PATCH 32/60] fix: rename dynamic links to timeline links --- .../doctype/communication/communication.js | 12 +- .../doctype/communication/communication.json | 12 +- .../doctype/communication/communication.py | 106 +++++++++++------- frappe/core/doctype/communication/email.py | 46 +------- .../doctype/email_account/email_account.py | 9 +- 5 files changed, 80 insertions(+), 105 deletions(-) diff --git a/frappe/core/doctype/communication/communication.js b/frappe/core/doctype/communication/communication.js index 5d17a98315..4734f6a11a 100644 --- a/frappe/core/doctype/communication/communication.js +++ b/frappe/core/doctype/communication/communication.js @@ -43,17 +43,17 @@ frappe.ui.form.on("Communication", { }); } - frm.add_custom_button(__("Relink"), function() { + frm.add_custom_button(__("Add Primary link"), function() { frm.trigger('show_relink_dialog'); - }); + }, "Links"); - frm.add_custom_button(__("Add link"), function() { + frm.add_custom_button(__("Add Timeline Link"), function() { frm.trigger('show_add_link_dialog'); - }); + }, "Links"); - frm.add_custom_button(__("Remove link"), function() { + frm.add_custom_button(__("Remove Timeline Link"), function() { frm.trigger('show_remove_link_dialog'); - }); + }, "Links"); if(frm.doc.communication_type=="Communication" && frm.doc.communication_medium == "Email" diff --git a/frappe/core/doctype/communication/communication.json b/frappe/core/doctype/communication/communication.json index b315a4ce98..6642fd60f4 100644 --- a/frappe/core/doctype/communication/communication.json +++ b/frappe/core/doctype/communication/communication.json @@ -47,7 +47,7 @@ "seen", "_user_tags", "timeline_links_sections", - "dynamic_links", + "timeline_links", "email_inbox", "message_id", "uid", @@ -203,6 +203,7 @@ "label": "Date" }, { + "default": "0", "fieldname": "read_receipt", "fieldtype": "Check", "label": "Sent Read Receipt", @@ -219,6 +220,7 @@ "read_only": 1 }, { + "default": "0", "fieldname": "read_by_recipient", "fieldtype": "Check", "label": "Read by Recipient", @@ -305,6 +307,7 @@ "read_only": 1 }, { + "default": "0", "fieldname": "seen", "fieldtype": "Check", "label": "Seen", @@ -348,6 +351,7 @@ "options": "Open\nSpam\nTrash" }, { + "default": "0", "fieldname": "has_attachment", "fieldtype": "Check", "hidden": 1, @@ -385,15 +389,15 @@ "label": "Timeline Links" }, { - "fieldname": "dynamic_links", + "fieldname": "timeline_links", "fieldtype": "Table", - "label": "Dynamic Links", + "label": "Timeline Links", "options": "Dynamic Link" } ], "icon": "fa fa-comment", "idx": 1, - "modified": "2019-05-13 19:55:35.757242", + "modified": "2019-05-17 13:52:44.471256", "modified_by": "Administrator", "module": "Core", "name": "Communication", diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index f25264ceb3..72885d8288 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -8,10 +8,11 @@ from frappe.model.document import Document from frappe.utils import validate_email_address, get_fullname, strip_html, cstr from frappe.core.doctype.communication.email import (validate_email, notify, _notify, update_parent_mins_to_first_response) +from frappe.core.utils import get_parent_doc from frappe.utils.bot import BotReply from frappe.utils import parse_addr from frappe.core.doctype.comment.comment import update_comment_in_doc - +from email.utils import parseaddr from collections import Counter exclude_from_linked_with = True @@ -56,10 +57,12 @@ class Communication(Document): self.set_status() self.set_sender_full_name() - self.validate_dynamic_links() - self.deduplicate_dynamic_links() validate_email(self) + if self.communication_medium == "Email": + self.set_timeline_links() + self.deduplicate_timeline_links() + def validate_reference(self): if self.reference_doctype and self.reference_name: if not self.reference_owner: @@ -73,14 +76,13 @@ class Communication(Document): # Prevent circular linking of Communication DocTypes if self.reference_doctype == "Communication": circular_linking = False - doc = get_parent_doc(self.reference_doctype, self.reference_name) - if doc: - while doc.reference_doctype == "Communication": - if doc: - if doc.reference_name == self.name: - circular_linking = True - break - doc = get_parent_doc(doc.reference_doctype, doc.reference_name) + doc = get_parent_doc(self) + while doc.reference_doctype == "Communication": + if get_parent_doc(doc).name==self.name: + circular_linking = True + break + doc = get_parent_doc(doc) + if circular_linking: frappe.throw(_("Please make sure the Reference Communication Docs are not circularly linked."), frappe.CircularLinkingError) @@ -234,11 +236,19 @@ class Communication(Document): frappe.db.commit() # Timeline Links - def deduplicate_dynamic_links(self): - if self.dynamic_links: + def set_timeline_links(self): + contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc]) + for contact_name in contacts: + self.add_link('Contact', contact_name) + + #link contact's dynamic links to communication + add_contact_links_to_communication(self, contact_name) + + def deduplicate_timeline_links(self): + if self.timeline_links: links, duplicate = [], False - for l in self.dynamic_links: + for l in self.timeline_links: t = (l.link_doctype, l.link_name) if not t in links: links.append(t) @@ -246,29 +256,12 @@ class Communication(Document): duplicate = True if duplicate: - del self.dynamic_links[:] # make it python 2 compatible as list.clear() is python 3 only + del self.timeline_links[:] # make it python 2 compatible as list.clear() is python 3 only for l in links: self.add_link(link_doctype=l[0], link_name=l[1]) - def validate_dynamic_links(self): - circular_linking = False - for dynamic_link in self.dynamic_links: - - # Prevent circular linking of Timeline DocTypes - if dynamic_link.link_doctype == "Communication": - doc = get_parent_doc(dynamic_link.link_doctype, dynamic_link.link_name) - if doc: - while doc.reference_doctype == "Communication": - if doc: - if doc.reference_name == self.name: - circular_linking = True - break - - if circular_linking: - frappe.throw(_("Please make sure the Timeline Communication Docs are not circularly linked."), frappe.CircularLinkingError) - def add_link(self, link_doctype, link_name, autosave=False): - self.append("dynamic_links", + self.append("timeline_links", { "link_doctype": link_doctype, "link_name": link_name @@ -279,12 +272,12 @@ class Communication(Document): self.save(ignore_permissions=True) def get_links(self): - return self.dynamic_links + return self.timeline_links def remove_link(self, link_doctype, link_name, autosave=False, ignore_permissions=True): - for l in self.dynamic_links: + for l in self.timeline_links: if l.link_doctype == link_doctype and l.link_name == link_name: - self.dynamic_links.remove(l) + self.timeline_links.remove(l) if autosave: self.save(ignore_permissions=ignore_permissions) @@ -292,7 +285,6 @@ class Communication(Document): def on_doctype_update(): """Add indexes in `tabCommunication`""" frappe.db.add_index("Communication", ["reference_doctype", "reference_name"]) - frappe.db.add_index("Communication", ["link_doctype", "link_name"]) frappe.db.add_index("Communication", ["status", "communication_type"]) def has_permission(doc, ptype, user): @@ -323,8 +315,38 @@ def get_permission_query_conditions_for_communication(user): return """tabCommunication.email_account in ({email_accounts})"""\ .format(email_accounts=','.join(email_accounts)) -def get_parent_doc(link_doctype, link_name): - """Returns document of `link_doctype`, `link_name`""" - if link_doctype and link_name: - parent_doc = frappe.get_doc(link_doctype, link_name) - return parent_doc if parent_doc else None \ No newline at end of file +def get_contacts(email_strings): + email_addrs = [] + + for email_string in email_strings: + if email_string: + for email in email_string.split(","): + parsed_email = parseaddr(email)[1] + if parsed_email: + email_addrs.append(parsed_email) + + contacts = [] + for email in email_addrs: + contact_name = frappe.db.get_value('Contact', {'email_id': email}) + + if not contact_name: + contact = frappe.get_doc({ + "doctype": "Contact", + "first_name": frappe.unscrub(email.split("@")[0]), + "email_id": email + }).insert(ignore_permissions=True) + contact_name = contact.name + + contacts.append(contact_name) + + return contacts + +def add_contact_links_to_communication(communication, contact_name): + contact_links = frappe.get_list("Dynamic Link", filters={ + "parenttype": "Contact", + "parent": contact_name + }, fields=["link_doctype", "link_name"]) + + if contact_links: + for contact_link in contact_links: + communication.add_link(contact_link.link_doctype, contact_link.link_name) \ No newline at end of file diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index aea2e148b8..aab0bc9bd6 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -17,7 +17,6 @@ import frappe.email.smtp import time from frappe import _ from frappe.utils.background_jobs import enqueue -from email.utils import parseaddr @frappe.whitelist() def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent", @@ -79,13 +78,6 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = # comm.reference_doctype = 'Communication' # comm.reference_name = comm.name - contacts = get_contacts([sender, recipients, cc, bcc]) - for contact_name in contacts: - comm.add_link('Contact', contact_name) - - #link contact's dynamic links to communication - add_contact_links_to_communication(comm, contact_name) - comm.save(ignore_permissions=True) if isinstance(attachments, string_types): @@ -567,40 +559,4 @@ def mark_email_as_seen(name=None): frappe.response["type"] = 'binary' frappe.response["filename"] = "imaginary_pixel.png" - frappe.response["filecontent"] = buffered_obj.getvalue() - -def get_contacts(email_strings): - email_addrs = [] - - for email_string in email_strings: - if email_string: - for email in email_string.split(","): - parsed_email = parseaddr(email)[1] - if parsed_email: - email_addrs.append(parsed_email) - - contacts = [] - for email in email_addrs: - contact_name = frappe.db.get_value('Contact', {'email_id': email}) - - if not contact_name: - contact = frappe.get_doc({ - "doctype": "Contact", - "first_name": frappe.unscrub(email.split("@")[0]), - "email_id": email - }).insert(ignore_permissions=True) - contact_name = contact.name - - contacts.append(contact_name) - - return contacts - -def add_contact_links_to_communication(communication, contact_name): - contact_links = frappe.get_list("Dynamic Link", filters={ - "parenttype": "Contact", - "parent": contact_name - }, fields=["link_doctype", "link_name"]) - - if contact_links: - for contact_link in contact_links: - communication.add_link(contact_link.link_doctype, contact_link.link_name) \ No newline at end of file + frappe.response["filecontent"] = buffered_obj.getvalue() \ No newline at end of file diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index fa8b409b1f..6ef94883f7 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -20,7 +20,7 @@ from datetime import datetime, timedelta from frappe.desk.form import assign_to from frappe.utils.user import get_system_managers from frappe.utils.background_jobs import enqueue, get_jobs -from frappe.core.doctype.communication.email import set_incoming_outgoing_accounts, get_contacts, add_contact_links_to_communication +from frappe.core.doctype.communication.email import set_incoming_outgoing_accounts from frappe.utils.scheduler import log from frappe.utils.html_utils import clean_email_html @@ -386,13 +386,6 @@ class EmailAccount(Document): users = list(set([ user.get("parent") for user in users ])) communication._seen = json.dumps(users) - contacts = get_contacts([email.from_email, email.mail.get("To"), email.mail.get("CC"), email.from_email]) - for contact_name in contacts: - communication.add_link('Contact', contact_name) - - #link contact's dynamic links to communication - add_contact_links_to_communication(communication, contact_name) - communication.flags.in_receive = True communication.insert(ignore_permissions=True) From dbd0a79ffc7e3672e42de6fd635ac4951e6470dd Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Fri, 17 May 2019 14:26:53 +0530 Subject: [PATCH 33/60] patch: move timeline and link to dynamic links --- .../v12_0/move_timeline_links_to_dynamic_links.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py index 0bb266269e..9a22f2d536 100644 --- a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py +++ b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py @@ -8,13 +8,23 @@ def execute(): fields=[ "name", "creation", "modified", "modified_by", "timeline_doctype", "timeline_name", + "link_doctype", "link_name" ]): + counter = 1 if communication.timeline_doctype and communication.timeline_name: comm_lists.append(( - "1", frappe.generate_hash(length=10), "dynamic_links", "Communication", + counter, frappe.generate_hash(length=10), "timeline_links", "Communication", communication.name, communication.timeline_doctype, communication.timeline_name, communication.creation, communication.modified, communication.modified_by )) + counter += 1 + + if communication.link_doctype and communication.link_name: + comm_lists.append(( + counter, frappe.generate_hash(length=10), "timeline_links", "Communication", + communication.name, communication.link_doctype, communication.link_name, + communication.creation, communication.modified, communication.modified_by + )) for comm_list in comm_lists: frappe.db.sql(""" From 9f02f4284d156a2b9f4f6e197fd73295c513d642 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Sat, 18 May 2019 00:35:17 +0530 Subject: [PATCH 34/60] patch: optimisations --- .../doctype/communication/communication.json | 19 +---- .../move_timeline_links_to_dynamic_links.py | 73 ++++++++++++++----- 2 files changed, 55 insertions(+), 37 deletions(-) diff --git a/frappe/core/doctype/communication/communication.json b/frappe/core/doctype/communication/communication.json index 6642fd60f4..9492bd205b 100644 --- a/frappe/core/doctype/communication/communication.json +++ b/frappe/core/doctype/communication/communication.json @@ -41,8 +41,6 @@ "user", "column_break_27", "email_template", - "link_doctype", - "link_name", "unread_notification_sent", "seen", "_user_tags", @@ -285,20 +283,6 @@ "fieldname": "column_break_27", "fieldtype": "Column Break" }, - { - "fieldname": "link_doctype", - "fieldtype": "Link", - "label": "Link DocType", - "options": "DocType", - "read_only": 1 - }, - { - "fieldname": "link_name", - "fieldtype": "Dynamic Link", - "label": "Link Name", - "options": "link_doctype", - "read_only": 1 - }, { "default": "0", "fieldname": "unread_notification_sent", @@ -397,7 +381,7 @@ ], "icon": "fa fa-comment", "idx": 1, - "modified": "2019-05-17 13:52:44.471256", + "modified": "2019-05-18 00:34:57.100559", "modified_by": "Administrator", "module": "Core", "name": "Communication", @@ -432,6 +416,7 @@ } ], "search_fields": "subject", + "sort_field": "modified", "sort_order": "DESC", "title_field": "subject", "track_changes": 1, diff --git a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py index 9a22f2d536..d8d249ae73 100644 --- a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py +++ b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py @@ -4,32 +4,65 @@ import frappe def execute(): comm_lists = [] - for communication in frappe.get_list("Communication", filters={"communication_medium": "Email"}, - fields=[ - "name", "creation", "modified", "modified_by", - "timeline_doctype", "timeline_name", - "link_doctype", "link_name" - ]): + for communication in frappe.db.sql(""" + Select + `tabCommunication`.name, `tabCommunication`.creation, `tabCommunication`.modified, + `tabCommunication`.modified_by,`tabCommunication`.timeline_doctype, `tabCommunication`.timeline_name, + `tabCommunication`.link_doctype, `tabCommunication`.link_name + from `tabCommunication` + where `tabCommunication`.communication_medium='Email' + """, as_dict=True): counter = 1 if communication.timeline_doctype and communication.timeline_name: - comm_lists.append(( - counter, frappe.generate_hash(length=10), "timeline_links", "Communication", - communication.name, communication.timeline_doctype, communication.timeline_name, - communication.creation, communication.modified, communication.modified_by - )) + comm_lists.append( + { + "idx": counter, + "name": frappe.generate_hash(length=10), + "parentfield": "timeline_links", + "parenttype": "Communication", + "parent": communication.name, + "link_doctype": communication.timeline_doctype, + "link_name": communication.timeline_name, + "creation": communication.creation, + "modified": communication.modified, + "modified_by": communication.modified_by + } + ) counter += 1 if communication.link_doctype and communication.link_name: - comm_lists.append(( - counter, frappe.generate_hash(length=10), "timeline_links", "Communication", - communication.name, communication.link_doctype, communication.link_name, - communication.creation, communication.modified, communication.modified_by - )) + comm_lists.append( + { + "idx": counter, + "name": frappe.generate_hash(length=10), + "parentfield": "timeline_links", + "parenttype": "Communication", + "parent": communication.name, + "link_doctype": communication.link_doctype, + "link_name": communication.link_name, + "creation": communication.creation, + "modified": communication.modified, + "modified_by": communication.modified_by + } + ) for comm_list in comm_lists: frappe.db.sql(""" - insert into table `tabDynamic Link` (idx, name, parentfield, parenttype, parent, link_doctype, link_name, creation, modified, modified_by) - values %(values)s""", - { - "values": comm_list + INSERT INTO `tabDynamic Link` + (`idx`, `name`, `parentfield`, `parenttype`, `parent`, `link_doctype`, `link_name`, `creation`, + `modified`, `modified_by`) + VALUES + (%(idx)s, %(name)s, %(parentfield)s, %(parenttype)s, %(parent)s, %(link_doctype)s, %(link_name)s,%(creation)s, + %(modified)s, %(modified_by)s) + """,{ + "idx": comm_list.get("idx"), + "name": comm_list.get("name"), + "parentfield": comm_list.get("parentfield"), + "parenttype": comm_list.get("parenttype"), + "parent": comm_list.get("parent"), + "link_doctype": comm_list.get("link_doctype"), + "link_name": comm_list.get("link_name"), + "creation": comm_list.get("creation"), + "modified": comm_list.get("modified"), + "modified_by": comm_list.get("modified_by") }) \ No newline at end of file From 0050fb58ffe13f9afb7131768e237c16512c671b Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Sat, 18 May 2019 10:40:55 +0530 Subject: [PATCH 35/60] fix: remove clear reference links from delete doc --- frappe/model/delete_doc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 73c6719d2b..8a557f3b3f 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -280,7 +280,6 @@ def delete_dynamic_links(doctype, name): # unlink communications clear_timeline_references(doctype, name) clear_references('Communication', doctype, name) - clear_references('Communication', doctype, name, 'link_doctype', 'link_name') clear_references('Activity Log', doctype, name) clear_references('Activity Log', doctype, name, 'timeline_doctype', 'timeline_name') From d6d0bc1cb34895fb97baaa7aba3bfd1ffdba3bf2 Mon Sep 17 00:00:00 2001 From: Prssanna Desai Date: Thu, 16 May 2019 16:45:05 +0530 Subject: [PATCH 36/60] Allow popover to be hovered on and other fixes --- frappe/public/js/frappe/ui/link_preview.js | 44 +++++++++++++++------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/frappe/public/js/frappe/ui/link_preview.js b/frappe/public/js/frappe/ui/link_preview.js index 538b187d9f..962adcf344 100644 --- a/frappe/public/js/frappe/ui/link_preview.js +++ b/frappe/public/js/frappe/ui/link_preview.js @@ -28,7 +28,9 @@ frappe.ui.LinkPreview = class { if (this.is_link) { this.doctype = this.element.attr('data-doctype'); this.name = this.element.attr('data-name'); + this.href = this.element.attr('href'); } else { + this.href = this.element.parents('.control-input-wrapper').find('.control-value a').attr('href'); // input this.doctype = this.element.attr('data-target'); this.name = this.element.val(); @@ -36,7 +38,11 @@ frappe.ui.LinkPreview = class { } setup_popover_control(e) { - if(!this.popover) { + //If control field value is changed, new popover has to be created + this.element.on('change',()=> { + this.new_popover = true; + }); + if(!this.popover || this.new_popover) { this.get_preview_fields().then(preview_fields => { if(preview_fields.length) { this.data_timeout = setTimeout(() => { @@ -49,13 +55,14 @@ frappe.ui.LinkPreview = class { if (this.element.is(':focus')) { return; } - this.popover.show(); + this.show_popover(e); }, 1000); } this.handle_popover_hide(); } create_popover(e, preview_fields) { + this.new_popover = false; if (this.element.is(':focus')) { return; } @@ -73,31 +80,37 @@ frappe.ui.LinkPreview = class { } else { this.init_preview_popover(preview_data); } - - if(!this.is_link) { - var left = e.pageX; - this.element.popover('show'); - var width = $('.popover').width(); - $('.control-field-popover').css('left', (left-(width/2)) + 'px'); - } else { - this.element.popover('show'); - } + this.show_popover(e); }, 1000); } }); } + show_popover(e) { + if(!this.is_link) { + var left = e.pageX; + this.element.popover('show'); + var width = $('.popover').width(); + $('.control-field-popover').css('left', (left-(width/2)) + 'px'); + } else { + this.element.popover('show'); + } + } + handle_popover_hide() { $(document.body).on('mouseout', this.LINK_CLASSES, () => { - this.link_hovered = false; + // To allow popover to be hovered on + if (!$('.popover:hover').length) { + this.link_hovered = false; + } if(this.data_timeout) { clearTimeout(this.data_timeout); } if (this.popover_timeout) { clearTimeout(this.popover_timeout); } - this.clear_all_popovers(); + if(!this.link_hovered) this.clear_all_popovers(); }); $(window).on('hashchange', () => { @@ -183,7 +196,7 @@ frappe.ui.LinkPreview = class { let id_html = ''; let content_html = ''; let meta = frappe.get_meta(this.doctype); - let title = preview_data[meta.title_field]; + let title = preview_data.title; if(preview_data[meta.image_field]) { let image_url = encodeURI(preview_data[meta.image_field]); @@ -197,6 +210,9 @@ frappe.ui.LinkPreview = class { if(title && title != preview_data.name) { id_html+= `${preview_data.name}`; } + if(!title) { + title = preview_data.name; + } Object.keys(preview_data).forEach(key => { if(key!=meta.image_field && key!='name' && key!=meta.title_field) { From defe1b2eef0da56261673d0c2887c391dce97037 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Sat, 18 May 2019 13:37:00 +0530 Subject: [PATCH 37/60] fix: miscellaneous changes --- .../doctype/communication/communication.js | 2 +- frappe/desk/doctype/todo/todo.py | 7 +- frappe/desk/form/load.py | 3 +- frappe/patches.txt | 2 - .../move_timeline_links_to_dynamic_links.py | 85 ++++++------------- 5 files changed, 35 insertions(+), 64 deletions(-) diff --git a/frappe/core/doctype/communication/communication.js b/frappe/core/doctype/communication/communication.js index 4734f6a11a..180ba8d25c 100644 --- a/frappe/core/doctype/communication/communication.js +++ b/frappe/core/doctype/communication/communication.js @@ -43,7 +43,7 @@ frappe.ui.form.on("Communication", { }); } - frm.add_custom_button(__("Add Primary link"), function() { + frm.add_custom_button(__("Change Reference Link"), function() { frm.trigger('show_relink_dialog'); }, "Links"); diff --git a/frappe/desk/doctype/todo/todo.py b/frappe/desk/doctype/todo/todo.py index 45c3874806..051d26d7be 100644 --- a/frappe/desk/doctype/todo/todo.py +++ b/frappe/desk/doctype/todo/todo.py @@ -43,8 +43,11 @@ class ToDo(Document): def on_trash(self): # unlink todo from linked comments - frappe.db.sql("""update `tabCommunication` set link_doctype=null, link_name=null - where link_doctype=%(doctype)s and link_name=%(name)s""", {"doctype": self.doctype, "name": self.name}) + frappe.db.sql(""" + delete from `tabDynamic Link` + where link_doctype=%(doctype)s and link_name=%(name)s""", { + "doctype": self.doctype, "name": self.name + }) self.update_in_reference() diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 102972f331..2985a8858e 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -166,8 +166,7 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields= `tabCommunication`.sender, `tabCommunication`.sender_full_name, `tabCommunication`.cc, `tabCommunication`.bcc, `tabCommunication`.creation, `tabCommunication`.subject, `tabCommunication`.delivery_status, `tabCommunication`._liked_by, `tabCommunication`.reference_doctype, `tabCommunication`.reference_name, - `tabCommunication`.link_doctype, `tabCommunication`.link_name, `tabCommunication`.read_by_recipient, - `tabCommunication`.rating + `tabCommunication`.read_by_recipient, `tabCommunication`.rating ''' conditions = ''' diff --git a/frappe/patches.txt b/frappe/patches.txt index 15f9ad8c73..911e793687 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -241,6 +241,4 @@ frappe.patches.v12_0.reset_home_settings frappe.patches.v12_0.update_print_format_type frappe.patches.v11_0.remove_doctype_user_permissions_for_page_and_report #2019-05-01 frappe.patches.v12_0.remove_feedback_rating -execute:frappe.reload_doc('core', 'doctype', 'communication') -execute:frappe.reload_doc('core', 'doctype', 'dynamic_link') frappe.patches.v12_0.move_timeline_links_to_dynamic_links \ No newline at end of file diff --git a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py index d8d249ae73..9d6cfffb84 100644 --- a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py +++ b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py @@ -3,66 +3,37 @@ from __future__ import unicode_literals import frappe def execute(): - comm_lists = [] - for communication in frappe.db.sql(""" - Select - `tabCommunication`.name, `tabCommunication`.creation, `tabCommunication`.modified, - `tabCommunication`.modified_by,`tabCommunication`.timeline_doctype, `tabCommunication`.timeline_name, - `tabCommunication`.link_doctype, `tabCommunication`.link_name - from `tabCommunication` - where `tabCommunication`.communication_medium='Email' - """, as_dict=True): + frappe.reload_doc('core', 'doctype', 'communication') + + sql_query = """INSERT INTO `tabDynamic Link` + (`idx`, `name`, `parentfield`, `parenttype`, `parent`, `link_doctype`, `link_name`, `creation`, + `modified`, `modified_by`) + VALUES """ + + communications = frappe.db.sql(""" + Select + `tabCommunication`.name, `tabCommunication`.creation, `tabCommunication`.modified, + `tabCommunication`.modified_by,`tabCommunication`.timeline_doctype, `tabCommunication`.timeline_name, + `tabCommunication`.link_doctype, `tabCommunication`.link_name + from `tabCommunication` + where `tabCommunication`.communication_medium='Email' + """, as_dict=True) + + for communication in communications: counter = 1 if communication.timeline_doctype and communication.timeline_name: - comm_lists.append( - { - "idx": counter, - "name": frappe.generate_hash(length=10), - "parentfield": "timeline_links", - "parenttype": "Communication", - "parent": communication.name, - "link_doctype": communication.timeline_doctype, - "link_name": communication.timeline_name, - "creation": communication.creation, - "modified": communication.modified, - "modified_by": communication.modified_by - } - ) + sql_query += str(( + counter, frappe.generate_hash(length=10), "timeline_links", "Communication", communication.name, + communication.timeline_doctype, communication.timeline_name, communication.creation, + communication.modified, communication.modified_by + )) + """, """ counter += 1 if communication.link_doctype and communication.link_name: - comm_lists.append( - { - "idx": counter, - "name": frappe.generate_hash(length=10), - "parentfield": "timeline_links", - "parenttype": "Communication", - "parent": communication.name, - "link_doctype": communication.link_doctype, - "link_name": communication.link_name, - "creation": communication.creation, - "modified": communication.modified, - "modified_by": communication.modified_by - } - ) + sql_query += str(( + counter, frappe.generate_hash(length=10), "timeline_links", "Communication", communication.name, + communication.link_doctype, communication.link_name, communication.creation, + communication.modified, communication.modified_by + )) + """, """ - for comm_list in comm_lists: - frappe.db.sql(""" - INSERT INTO `tabDynamic Link` - (`idx`, `name`, `parentfield`, `parenttype`, `parent`, `link_doctype`, `link_name`, `creation`, - `modified`, `modified_by`) - VALUES - (%(idx)s, %(name)s, %(parentfield)s, %(parenttype)s, %(parent)s, %(link_doctype)s, %(link_name)s,%(creation)s, - %(modified)s, %(modified_by)s) - """,{ - "idx": comm_list.get("idx"), - "name": comm_list.get("name"), - "parentfield": comm_list.get("parentfield"), - "parenttype": comm_list.get("parenttype"), - "parent": comm_list.get("parent"), - "link_doctype": comm_list.get("link_doctype"), - "link_name": comm_list.get("link_name"), - "creation": comm_list.get("creation"), - "modified": comm_list.get("modified"), - "modified_by": comm_list.get("modified_by") - }) \ No newline at end of file + frappe.db.sql(sql_query) From 6c3130dc77c4e5a65fe8ebd46156791f9325a45f Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Sat, 18 May 2019 16:42:56 +0530 Subject: [PATCH 38/60] fix: Allow variables to be overridden using theme (#7514) --- frappe/public/scss/variables.scss | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/frappe/public/scss/variables.scss b/frappe/public/scss/variables.scss index ac9bf6cf03..6ee7cda884 100644 --- a/frappe/public/scss/variables.scss +++ b/frappe/public/scss/variables.scss @@ -1,20 +1,20 @@ -$gray-100: #fafbfc; -$gray-150: #f5f7fa; -$gray-200: #ebecf1; -$gray-300: #d1d8dd; -$gray-400: #ced4da; -$gray-500: #adb5bd; -$gray-600: #8d99a6; -$gray-700: #495057; -$gray-800: #36414c; -$gray-900: #2e3338; -$primary: #5e64ff; +$gray-100: #fafbfc !default; +$gray-150: #f5f7fa !default; +$gray-200: #ebecf1 !default; +$gray-300: #d1d8dd !default; +$gray-400: #ced4da !default; +$gray-500: #adb5bd !default; +$gray-600: #8d99a6 !default; +$gray-700: #495057 !default; +$gray-800: #36414c !default; +$gray-900: #2e3338 !default; +$primary: #5e64ff !default; -$black: #000; +$black: #000 !default; -$body-color: $gray-800; -$text-muted: $gray-600; -$border-color: $gray-200; +$body-color: $gray-800 !default; +$text-muted: $gray-600 !default; +$border-color: $gray-200 !default; @import "~bootstrap/scss/functions"; @import "~bootstrap/scss/variables"; From 6403cf45ae2baf2110cb5cb2d03a324affcc87bb Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Sat, 18 May 2019 18:54:47 +0530 Subject: [PATCH 39/60] patch: optimisations --- .../move_timeline_links_to_dynamic_links.py | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py index 9d6cfffb84..507ed05e18 100644 --- a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py +++ b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py @@ -5,11 +5,6 @@ import frappe def execute(): frappe.reload_doc('core', 'doctype', 'communication') - sql_query = """INSERT INTO `tabDynamic Link` - (`idx`, `name`, `parentfield`, `parenttype`, `parent`, `link_doctype`, `link_name`, `creation`, - `modified`, `modified_by`) - VALUES """ - communications = frappe.db.sql(""" Select `tabCommunication`.name, `tabCommunication`.creation, `tabCommunication`.modified, @@ -19,21 +14,32 @@ def execute(): where `tabCommunication`.communication_medium='Email' """, as_dict=True) - for communication in communications: + values = [] + for count, communication in enumerate(communications): counter = 1 if communication.timeline_doctype and communication.timeline_name: - sql_query += str(( - counter, frappe.generate_hash(length=10), "timeline_links", "Communication", communication.name, - communication.timeline_doctype, communication.timeline_name, communication.creation, - communication.modified, communication.modified_by - )) + """, """ + values.append("""({0}, '{1}', 'timeline_links', 'Communication', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}')""".format( + counter, frappe.generate_hash(length=10), communication.name, communication.timeline_doctype, + communication.timeline_name, communication.creation, communication.modified, communication.modified_by + )) counter += 1 - if communication.link_doctype and communication.link_name: - sql_query += str(( - counter, frappe.generate_hash(length=10), "timeline_links", "Communication", communication.name, - communication.link_doctype, communication.link_name, communication.creation, - communication.modified, communication.modified_by - )) + """, """ - - frappe.db.sql(sql_query) + values.append("""({0}, '{1}', 'timeline_links', 'Communication', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}')""".format( + counter, frappe.generate_hash(length=10), communication.name, communication.link_doctype, + communication.link_name, communication.creation, communication.modified, communication.modified_by + )) + if count % 200 == 0 or count == len(communications) - 1: + print(""" + INSERT INTO `tabDynamic Link` + (`idx`, `name`, `parentfield`, `parenttype`, `parent`, `link_doctype`, `link_name`, `creation`, + `modified`, `modified_by`) + VALUES {0} + """.format(", ".join([d for d in values]))) + frappe.db.sql(""" + INSERT INTO `tabDynamic Link` + (`idx`, `name`, `parentfield`, `parenttype`, `parent`, `link_doctype`, `link_name`, `creation`, + `modified`, `modified_by`) + VALUES {0} + """.format(", ".join([d for d in values]))) + # frappe.throw("YA") + values = [] \ No newline at end of file From c52c1611cb462a7f7bf12f7735634db3e4e7edee Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Sat, 18 May 2019 19:17:00 +0530 Subject: [PATCH 40/60] patch: optimisations --- .../v12_0/move_timeline_links_to_dynamic_links.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py index 507ed05e18..cbc3846604 100644 --- a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py +++ b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py @@ -15,6 +15,7 @@ def execute(): """, as_dict=True) values = [] + for count, communication in enumerate(communications): counter = 1 if communication.timeline_doctype and communication.timeline_name: @@ -28,18 +29,11 @@ def execute(): counter, frappe.generate_hash(length=10), communication.name, communication.link_doctype, communication.link_name, communication.creation, communication.modified, communication.modified_by )) - if count % 200 == 0 or count == len(communications) - 1: - print(""" - INSERT INTO `tabDynamic Link` - (`idx`, `name`, `parentfield`, `parenttype`, `parent`, `link_doctype`, `link_name`, `creation`, - `modified`, `modified_by`) - VALUES {0} - """.format(", ".join([d for d in values]))) + if count % 1000 == 0 or count == len(communications) - 1: frappe.db.sql(""" INSERT INTO `tabDynamic Link` (`idx`, `name`, `parentfield`, `parenttype`, `parent`, `link_doctype`, `link_name`, `creation`, `modified`, `modified_by`) VALUES {0} """.format(", ".join([d for d in values]))) - # frappe.throw("YA") values = [] \ No newline at end of file From 05b3050b630501ef11c413bb280492eb4d32c063 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Sun, 19 May 2019 00:39:11 +0530 Subject: [PATCH 41/60] patch: batch 10000 query for patch --- frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py index cbc3846604..ff61208bf4 100644 --- a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py +++ b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py @@ -29,7 +29,7 @@ def execute(): counter, frappe.generate_hash(length=10), communication.name, communication.link_doctype, communication.link_name, communication.creation, communication.modified, communication.modified_by )) - if count % 1000 == 0 or count == len(communications) - 1: + if count % 10000 == 0 or count == len(communications) - 1: frappe.db.sql(""" INSERT INTO `tabDynamic Link` (`idx`, `name`, `parentfield`, `parenttype`, `parent`, `link_doctype`, `link_name`, `creation`, From ba7af52caa3cf201b487f6e8b785aa1403b65a33 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Sun, 19 May 2019 17:44:46 +0530 Subject: [PATCH 42/60] fix: Module view fix for custom reports --- frappe/desk/moduleview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/moduleview.py b/frappe/desk/moduleview.py index a9a460b124..ccde3aad40 100644 --- a/frappe/desk/moduleview.py +++ b/frappe/desk/moduleview.py @@ -383,7 +383,7 @@ def get_report_list(module, is_standard="No"): out.append({ "type": "report", "doctype": r.ref_doctype, - "is_query_report": 1 if r.report_type in ("Query Report", "Script Report") else 0, + "is_query_report": 1 if r.report_type in ("Query Report", "Script Report", "Custom Report") else 0, "label": _(r.name), "name": r.name }) From a873340d4e928e36c7182fa2e9dac153b1796859 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 20 May 2019 12:19:42 +0530 Subject: [PATCH 43/60] fix: Create new button for dynamic links (#7511) * fix: Create new button for dynamic links * fix: Use get_options for fetching options --- frappe/public/js/frappe/form/controls/link.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index a7ed3f8a72..750ad7a64c 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -168,13 +168,12 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ } if(!me.df.only_select) { - if(frappe.model.can_create(doctype) - && me.df.fieldtype !== "Dynamic Link") { + if(frappe.model.can_create(doctype)) { // new item r.results.push({ label: "" + " " - + __("Create a new {0}", [__(me.df.options)]) + + __("Create a new {0}", [__(me.get_options())]) + "", value: "create_new__link_option", action: me.new_doc From 1a58c13966cb40d7ea7e04af5af50fde310ecb52 Mon Sep 17 00:00:00 2001 From: jibin jose Date: Thu, 16 May 2019 16:03:17 +0530 Subject: [PATCH 44/60] fix(postgres): Make db queries postgres compatible --- .../doctype/communication/communication.py | 4 +- frappe/database/postgres/schema.py | 17 +++++++- frappe/desk/doctype/todo/todo.py | 4 +- frappe/model/__init__.py | 42 +++++++++++++------ .../v11_0/set_default_letter_head_source.py | 2 +- frappe/patches/v12_0/init_desk_settings.py | 2 +- .../setup_comments_from_communications.py | 2 +- 7 files changed, 51 insertions(+), 22 deletions(-) diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index 77ccefba71..4f9f07d803 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -265,8 +265,8 @@ def get_permission_query_conditions_for_communication(user): distinct=True, order_by="idx") if not accounts: - return """tabCommunication.communication_medium!='Email'""" + return """`tabCommunication`.communication_medium!='Email'""" email_accounts = [ '"%s"'%account.get("email_account") for account in accounts ] - return """tabCommunication.email_account in ({email_accounts})"""\ + return """`tabCommunication`.email_account in ({email_accounts})"""\ .format(email_accounts=','.join(email_accounts)) diff --git a/frappe/database/postgres/schema.py b/frappe/database/postgres/schema.py index 05b6b19a9a..b5129b60bb 100644 --- a/frappe/database/postgres/schema.py +++ b/frappe/database/postgres/schema.py @@ -40,7 +40,20 @@ class PostgresTable(DBTable): query.append("ADD COLUMN `{}` {}".format(col.fieldname, col.get_definition())) for col in self.change_type: - query.append("ALTER COLUMN `{}` TYPE {}".format(col.fieldname, get_definition(col.fieldtype, precision=col.precision, length=col.length))) + using_clause = "" + if col.fieldtype in ("Datetime"): + # The USING option of SET DATA TYPE can actually specify any expression + # involving the old values of the row + # read more https://www.postgresql.org/docs/9.1/sql-altertable.html + using_clause = "USING {}::timestamp without time zone".format(col.fieldname) + elif col.fieldtype in ("Check"): + using_clause = "USING {}::smallint".format(col.fieldname) + + query.append("ALTER COLUMN {0} TYPE {1} {2}".format( + col.fieldname, + get_definition(col.fieldtype, precision=col.precision, length=col.length), + using_clause) + ) for col in self.set_default: if col.fieldname=="name": @@ -93,4 +106,4 @@ class PostgresTable(DBTable): fieldname, self.table_name))) raise e else: - raise e \ No newline at end of file + raise e diff --git a/frappe/desk/doctype/todo/todo.py b/frappe/desk/doctype/todo/todo.py index 45c3874806..c9fa96c716 100644 --- a/frappe/desk/doctype/todo/todo.py +++ b/frappe/desk/doctype/todo/todo.py @@ -94,7 +94,7 @@ def get_permission_query_conditions(user): if "System Manager" in frappe.get_roles(user): return None else: - return """(tabToDo.owner = {user} or tabToDo.assigned_by = {user})"""\ + return """(`tabToDo`.owner = {user} or `tabToDo`.assigned_by = {user})"""\ .format(user=frappe.db.escape(user)) def has_permission(doc, user): @@ -108,4 +108,4 @@ def new_todo(description): frappe.get_doc({ 'doctype': 'ToDo', 'description': description - }).insert() \ No newline at end of file + }).insert() diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index 5f804e3c36..3a8868ec2c 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -76,27 +76,43 @@ def delete_fields(args_dict, delete=0): args_dict = { dt: [field names] } """ import frappe.utils - for dt in list(args_dict): + for dt in args_dict: fields = args_dict[dt] - if not fields: continue + if not fields: + continue - frappe.db.sql("""\ + frappe.db.sql(""" DELETE FROM `tabDocField` - WHERE parent=%s AND fieldname IN (%s) - """ % ('%s', ", ".join(['"' + f + '"' for f in fields])), dt) + WHERE parent='%s' AND fieldname IN (%s) + """ % (dt, ", ".join(["'{}'".format(f) for f in fields]))) - # Delete the data / column only if delete is specified - if not delete: continue + # Delete the data/column only if delete is specified + if not delete: + continue if frappe.db.get_value("DocType", dt, "issingle"): - frappe.db.sql("""\ + frappe.db.sql(""" DELETE FROM `tabSingles` - WHERE doctype=%s AND field IN (%s) - """ % ('%s', ", ".join(['"' + f + '"' for f in fields])), dt) + WHERE doctype='%s' AND field IN (%s) + """ % (dt, ", ".join(["'{}'".format(f) for f in fields]))) else: - existing_fields = frappe.db.sql("desc `tab%s`" % dt) + existing_fields = frappe.db.multisql({ + "mariadb": "DESC `tab%s`" % dt, + "postgres": """ + SELECT + COLUMN_NAME + FROM + information_schema.COLUMNS + WHERE + TABLE_NAME = 'tab%s'; + """ % dt, + }) existing_fields = existing_fields and [e[0] for e in existing_fields] or [] + fields_need_to_delete = set(fields) & set(existing_fields) + if not fields_need_to_delete: + continue query = "ALTER TABLE `tab%s` " % dt + \ - ", ".join(["DROP COLUMN `%s`" % f for f in fields if f in existing_fields]) - frappe.db.commit() + ", ".join(["DROP COLUMN `%s`" % f for f in fields_need_to_delete]) frappe.db.sql(query) + # commit the results to db + frappe.db.commit() diff --git a/frappe/patches/v11_0/set_default_letter_head_source.py b/frappe/patches/v11_0/set_default_letter_head_source.py index 069f4e3d2e..a43ea397e4 100644 --- a/frappe/patches/v11_0/set_default_letter_head_source.py +++ b/frappe/patches/v11_0/set_default_letter_head_source.py @@ -6,4 +6,4 @@ def execute(): frappe.reload_doctype('Letter Head') # source of all existing letter heads must be HTML - frappe.db.sql('update `tabLetter Head` set source = "HTML"') \ No newline at end of file + frappe.db.sql("update `tabLetter Head` set source = 'HTML'") diff --git a/frappe/patches/v12_0/init_desk_settings.py b/frappe/patches/v12_0/init_desk_settings.py index 782ced8a26..31c6cf9207 100644 --- a/frappe/patches/v12_0/init_desk_settings.py +++ b/frappe/patches/v12_0/init_desk_settings.py @@ -8,4 +8,4 @@ from frappe.desk.moduleview import get_onboard_items def execute(): """Reset the initial customizations for desk, with modules, indices and links.""" frappe.reload_doc("core", "doctype", "user") - frappe.db.sql("""update tabUser set home_settings = %s""", (''), debug=True) + frappe.db.sql("""update `tabUser` set home_settings = %s""", (''), debug=True) diff --git a/frappe/patches/v12_0/setup_comments_from_communications.py b/frappe/patches/v12_0/setup_comments_from_communications.py index 1a7a5aef84..b52304bc05 100644 --- a/frappe/patches/v12_0/setup_comments_from_communications.py +++ b/frappe/patches/v12_0/setup_comments_from_communications.py @@ -25,4 +25,4 @@ def execute(): new_comment.db_insert() # clean up - frappe.db.sql('delete from tabCommunication where communication_type = "Comment"') \ No newline at end of file + frappe.db.sql("delete from `tabCommunication` where communication_type = 'Comment'") From 480f1794d37c524893645e296e22a37490a2795e Mon Sep 17 00:00:00 2001 From: jibin jose Date: Mon, 20 May 2019 12:29:45 +0530 Subject: [PATCH 45/60] Make db query postgres compatible --- frappe/patches/v12_0/update_print_format_type.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/patches/v12_0/update_print_format_type.py b/frappe/patches/v12_0/update_print_format_type.py index 33035abc2a..577dc68d94 100644 --- a/frappe/patches/v12_0/update_print_format_type.py +++ b/frappe/patches/v12_0/update_print_format_type.py @@ -3,11 +3,11 @@ import frappe def execute(): frappe.db.sql(''' UPDATE `tabPrint Format` - SET `print_format_type` = "Jinja" - WHERE `print_format_type` in ("Server", "Client") + SET `print_format_type` = 'Jinja' + WHERE `print_format_type` in ('Server', 'Client') ''') frappe.db.sql(''' UPDATE `tabPrint Format` - SET `print_format_type` = "JS" - WHERE `print_format_type` = "Js" + SET `print_format_type` = 'JS' + WHERE `print_format_type` = 'Js' ''') From 127cf29a2037c7f33afdcc151526d84265dd1dc5 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Mon, 20 May 2019 13:04:16 +0530 Subject: [PATCH 46/60] tests: added new test cases --- .../doctype/communication/communication.json | 18 ++- .../doctype/communication/communication.py | 6 +- frappe/core/doctype/communication/email.py | 5 - .../communication/test_communication.py | 107 ++++++++++++++++++ .../move_timeline_links_to_dynamic_links.py | 57 ++++++---- 5 files changed, 157 insertions(+), 36 deletions(-) diff --git a/frappe/core/doctype/communication/communication.json b/frappe/core/doctype/communication/communication.json index 9492bd205b..3eeb63ceb9 100644 --- a/frappe/core/doctype/communication/communication.json +++ b/frappe/core/doctype/communication/communication.json @@ -368,6 +368,7 @@ "read_only": 1 }, { + "collapsible": 1, "fieldname": "timeline_links_sections", "fieldtype": "Section Break", "label": "Timeline Links" @@ -376,13 +377,14 @@ "fieldname": "timeline_links", "fieldtype": "Table", "label": "Timeline Links", - "options": "Dynamic Link" + "options": "Dynamic Link", + "permlevel": 2 } ], "icon": "fa fa-comment", "idx": 1, - "modified": "2019-05-18 00:34:57.100559", - "modified_by": "Administrator", + "modified": "2019-05-20 12:34:21.021632", + "modified_by": "faris@erpnext.com", "module": "Core", "name": "Communication", "owner": "Administrator", @@ -407,6 +409,16 @@ "role": "System Manager", "share": 1 }, + { + "email": 1, + "export": 1, + "permlevel": 2, + "print": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, { "delete": 1, "email": 1, diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index 72885d8288..c164f7f8e0 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -321,9 +321,9 @@ def get_contacts(email_strings): for email_string in email_strings: if email_string: for email in email_string.split(","): - parsed_email = parseaddr(email)[1] - if parsed_email: - email_addrs.append(parsed_email) + parsed_email = parseaddr(email)[1] + if parsed_email: + email_addrs.append(parsed_email) contacts = [] for email in email_addrs: diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index aab0bc9bd6..36377a90f7 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -73,11 +73,6 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "has_attachment": 1 if attachments else 0 }).insert(ignore_permissions=True) - # if not doctype: - # # if no reference given, then send it against the communication - # comm.reference_doctype = 'Communication' - # comm.reference_name = comm.name - comm.save(ignore_permissions=True) if isinstance(attachments, string_types): diff --git a/frappe/core/doctype/communication/test_communication.py b/frappe/core/doctype/communication/test_communication.py index 715afe2cb3..5b0ad3f3b0 100644 --- a/frappe/core/doctype/communication/test_communication.py +++ b/frappe/core/doctype/communication/test_communication.py @@ -71,3 +71,110 @@ class TestCommunication(unittest.TestCase): a.reference_name = c.name self.assertRaises(frappe.CircularLinkingError, a.save) + + def test_deduplication_timeline_links(self): + todo = frappe.get_doc({ + "doctype": "ToDo", + "description": "Test ToDo" + }).insert(ignore_permissions=True) + + comm = frappe.get_doc({ + "doctype": "Communication", + "communication_type": "Communication", + "content": "Deduplication of Links", + "communication_medium": "Email", + "timeline_links": [ + { + "link_doctype": "ToDo", + "link_name": todo.name + }, + { + "link_doctype": "ToDo", + "link_name": todo.name + } + ] + }).insert(ignore_permissions=True) + + comm = frappe.get_doc("Communication", comm.name) + + self.assertNotEqual(2, len(comm.timeline_links)) + + def test_contacts_attached(self): + contact_sender = frappe.get_doc({ + "doctype": "Contact", + "first_name": frappe.generate_hash(length=10), + "email_id": "comm_sender@example.com" + }).insert(ignore_permissions=True) + + contact_recipient = frappe.get_doc({ + "doctype": "Contact", + "first_name": frappe.generate_hash(length=10), + "email_id": "comm_recipient@example.com" + }).insert(ignore_permissions=True) + + contact_cc = frappe.get_doc({ + "doctype": "Contact", + "first_name": frappe.generate_hash(length=10), + "email_id": "comm_cc@example.com" + }).insert(ignore_permissions=True) + + comm = frappe.get_doc({ + "doctype": "Communication", + "communication_medium": "Email", + "subject": "Contacts Attached Test", + "sender": "comm_sender@example.com", + "recipients": "comm_recipient@example.com" + }).insert(ignore_permissions=True) + + comm = frappe.get_doc("Communication", comm.name) + + contact_links = [] + for timeline_link in comm.timeline_links: + contact_links.append(timeline_link.link_name) + + self.assertIn(contact_sender.name, contact_links) + self.assertIn(contact_recipient.name, contact_links) + self.assertIn(contact_cc.name, contact_links) + + def test_get_communication_data(self): + from frappe.desk.form.load import get_communication_data + + todo = frappe.get_doc({ + "doctype": "ToDo", + "description": "Test ToDo" + }).insert(ignore_permissions=True) + + comm_todo_1 = frappe.get_doc({ + "doctype": "Communication", + "communication_type": "Communication", + "content": "Test Get Communication Data 1", + "communication_medium": "Email", + "timeline_links": [ + { + "link_doctype": "ToDo", + "link_name": todo.name + } + ] + }).insert(ignore_permissions=True) + + comm_todo_2 = frappe.get_doc({ + "doctype": "Communication", + "communication_type": "Communication", + "content": "Test Get Communication Data 2", + "communication_medium": "Email", + "timeline_links": [ + { + "link_doctype": "ToDo", + "link_name": todo.name + } + ] + }).insert(ignore_permissions=True) + + comms = get_communication_data("ToDo", todo.name, as_dict=True) + + data = [] + for comm in comms: + data.append(comm.name) + + self.assertIn(comm_todo_1.name, data) + self.assertIn(comm_todo_2.name, data) \ No newline at end of file diff --git a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py index ff61208bf4..cf1d22f27f 100644 --- a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py +++ b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py @@ -6,34 +6,41 @@ def execute(): frappe.reload_doc('core', 'doctype', 'communication') communications = frappe.db.sql(""" - Select - `tabCommunication`.name, `tabCommunication`.creation, `tabCommunication`.modified, - `tabCommunication`.modified_by,`tabCommunication`.timeline_doctype, `tabCommunication`.timeline_name, - `tabCommunication`.link_doctype, `tabCommunication`.link_name - from `tabCommunication` - where `tabCommunication`.communication_medium='Email' - """, as_dict=True) - - values = [] + SELECT + `tabCommunication`.name, `tabCommunication`.creation, `tabCommunication`.modified, + `tabCommunication`.modified_by,`tabCommunication`.timeline_doctype, `tabCommunication`.timeline_name, + `tabCommunication`.link_doctype, `tabCommunication`.link_name + FROM `tabCommunication` + WHERE `tabCommunication`.communication_medium='Email' + """, as_dict=True) for count, communication in enumerate(communications): counter = 1 if communication.timeline_doctype and communication.timeline_name: - values.append("""({0}, '{1}', 'timeline_links', 'Communication', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}')""".format( - counter, frappe.generate_hash(length=10), communication.name, communication.timeline_doctype, - communication.timeline_name, communication.creation, communication.modified, communication.modified_by - )) + values = [ + counter, frappe.generate_hash(length=10), 'timeline_links', 'Communication', communication.name, + communication.timeline_doctype, communication.timeline_name, communication.creation, communication.modified, + communication.modified_by + ] + execute_query(values) counter += 1 + if communication.link_doctype and communication.link_name: - values.append("""({0}, '{1}', 'timeline_links', 'Communication', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}')""".format( - counter, frappe.generate_hash(length=10), communication.name, communication.link_doctype, - communication.link_name, communication.creation, communication.modified, communication.modified_by - )) - if count % 10000 == 0 or count == len(communications) - 1: - frappe.db.sql(""" - INSERT INTO `tabDynamic Link` - (`idx`, `name`, `parentfield`, `parenttype`, `parent`, `link_doctype`, `link_name`, `creation`, - `modified`, `modified_by`) - VALUES {0} - """.format(", ".join([d for d in values]))) - values = [] \ No newline at end of file + values = [ + counter, frappe.generate_hash(length=10), 'timeline_links', 'Communication', communication.name, + communication.link_doctype, communication.link_name, communication.creation, communication.modified, + communication.modified_by + ] + execute_query(values) + +def execute_query(values): + try: + frappe.db.sql(""" + INSERT INTO `tabDynamic Link` + (`idx`, `name`, `parentfield`, `parenttype`, `parent`, `link_doctype`, `link_name`, `creation`, + `modified`, `modified_by`) + VALUES ({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}) + """.format(values[0], values[1], values[2], values[3], values[4], values[5], values[6], values[7], values[8], values[9]) + except IntegrityError as e: + values[1] = frappe.generate_hash(length=10) + execute_query(values) From c86ba036327f89c174dd7d4f9d9df630b10fe3c8 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Mon, 20 May 2019 13:38:01 +0530 Subject: [PATCH 47/60] tests: fix test cases --- frappe/core/doctype/communication/test_communication.py | 3 ++- frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/communication/test_communication.py b/frappe/core/doctype/communication/test_communication.py index 5b0ad3f3b0..77eb066b1d 100644 --- a/frappe/core/doctype/communication/test_communication.py +++ b/frappe/core/doctype/communication/test_communication.py @@ -123,7 +123,8 @@ class TestCommunication(unittest.TestCase): "communication_medium": "Email", "subject": "Contacts Attached Test", "sender": "comm_sender@example.com", - "recipients": "comm_recipient@example.com" + "recipients": "comm_recipient@example.com", + "cc": "comm_cc@example.com" }).insert(ignore_permissions=True) comm = frappe.get_doc("Communication", comm.name) diff --git a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py index cf1d22f27f..624be6ae40 100644 --- a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py +++ b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py @@ -41,6 +41,6 @@ def execute_query(values): `modified`, `modified_by`) VALUES ({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}) """.format(values[0], values[1], values[2], values[3], values[4], values[5], values[6], values[7], values[8], values[9]) - except IntegrityError as e: + except Exception as e: values[1] = frappe.generate_hash(length=10) execute_query(values) From b4e561c047057fcd5c918e0b143257bd8b1b8ef7 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Mon, 20 May 2019 14:15:57 +0530 Subject: [PATCH 48/60] tests: fix test cases --- .../doctype/communication/communication.js | 80 +------------------ .../doctype/communication/communication.json | 6 +- .../doctype/communication/communication.py | 1 + .../communication/test_communication.py | 60 ++++++-------- 4 files changed, 31 insertions(+), 116 deletions(-) diff --git a/frappe/core/doctype/communication/communication.js b/frappe/core/doctype/communication/communication.js index 180ba8d25c..924c29bee2 100644 --- a/frappe/core/doctype/communication/communication.js +++ b/frappe/core/doctype/communication/communication.js @@ -43,17 +43,9 @@ frappe.ui.form.on("Communication", { }); } - frm.add_custom_button(__("Change Reference Link"), function() { + frm.add_custom_button(__("Relink"), function() { frm.trigger('show_relink_dialog'); - }, "Links"); - - frm.add_custom_button(__("Add Timeline Link"), function() { - frm.trigger('show_add_link_dialog'); - }, "Links"); - - frm.add_custom_button(__("Remove Timeline Link"), function() { - frm.trigger('show_remove_link_dialog'); - }, "Links"); + }); if(frm.doc.communication_type=="Communication" && frm.doc.communication_medium == "Email" @@ -149,74 +141,6 @@ frappe.ui.form.on("Communication", { d.show(); }, - show_add_link_dialog: function(frm){ - var d = new frappe.ui.Dialog ({ - title: __("Add new link to Communication"), - fields: [{ - "fieldtype": "Link", - "options": "DocType", - "label": __("Document Type"), - "fieldname": "link_doctype", - "reqd": 1 - }, - { - "fieldtype": "Dynamic Link", - "options": "link_doctype", - "label": __("Document Name"), - "fieldname": "link_name", - "reqd": 1 - }], - primary_action: ({ link_doctype, link_name }) => { - d.hide(); - frm.call('add_link', { - link_doctype, - link_name, - autosave: true - }).then(() => frm.refresh()); - }, - primary_action_label: __('Add Link') - }); - d.fields_dict.link_doctype.get_query = function() { - return { - "filters": { - "name": ["!=", "Communication"], - } - }; - }; - d.show(); - }, - - show_remove_link_dialog: function(frm){ - let options = ''; - - for(var link in frm.doc.dynamic_links){ - let dynamic_link = frm.doc.dynamic_links[link]; - options += '\n' + dynamic_link.link_doctype + ': ' + dynamic_link.link_name; - } - - var d = new frappe.ui.Dialog ({ - title: __("Remove link from Communication"), - fields: [{ - "fieldtype": "Select", - "options": options, - "label": __("Link"), - "fieldname": "link", - "reqd": 1 - }], - primary_action: ({ link }) => { - d.hide(); - frm.call('remove_link', { - link_doctype: link.split(":")[0].trim(), - link_name: link.split(":")[1].trim(), - autosave: true, - ignore_permissions: false - }).then(() => frm.refresh()); - }, - primary_action_label: __('Remove Link') - }); - d.show(); - }, - mark_as_read_unread: function(frm) { var action = frm.doc.seen? "Unread": "Read"; var flag = "(\\SEEN)"; diff --git a/frappe/core/doctype/communication/communication.json b/frappe/core/doctype/communication/communication.json index 3eeb63ceb9..68a0adb634 100644 --- a/frappe/core/doctype/communication/communication.json +++ b/frappe/core/doctype/communication/communication.json @@ -383,8 +383,8 @@ ], "icon": "fa fa-comment", "idx": 1, - "modified": "2019-05-20 12:34:21.021632", - "modified_by": "faris@erpnext.com", + "modified": "2019-05-20 14:14:01.514493", + "modified_by": "Administrator", "module": "Core", "name": "Communication", "owner": "Administrator", @@ -410,10 +410,12 @@ "share": 1 }, { + "delete": 1, "email": 1, "export": 1, "permlevel": 2, "print": 1, + "read": 1, "report": 1, "role": "System Manager", "share": 1, diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index c164f7f8e0..aa70024eca 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -238,6 +238,7 @@ class Communication(Document): # Timeline Links def set_timeline_links(self): contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc]) + print(contacts) for contact_name in contacts: self.add_link('Contact', contact_name) diff --git a/frappe/core/doctype/communication/test_communication.py b/frappe/core/doctype/communication/test_communication.py index 77eb066b1d..a783157dc0 100644 --- a/frappe/core/doctype/communication/test_communication.py +++ b/frappe/core/doctype/communication/test_communication.py @@ -73,28 +73,23 @@ class TestCommunication(unittest.TestCase): self.assertRaises(frappe.CircularLinkingError, a.save) def test_deduplication_timeline_links(self): - todo = frappe.get_doc({ - "doctype": "ToDo", - "description": "Test ToDo" + note = frappe.get_doc({ + "doctype": "Note", + "title": "deduplication timeline links", + "content": "deduplication timeline links" }).insert(ignore_permissions=True) comm = frappe.get_doc({ "doctype": "Communication", "communication_type": "Communication", "content": "Deduplication of Links", - "communication_medium": "Email", - "timeline_links": [ - { - "link_doctype": "ToDo", - "link_name": todo.name - }, - { - "link_doctype": "ToDo", - "link_name": todo.name - } - ] + "communication_medium": "Email" }).insert(ignore_permissions=True) + #adding same link twice + comm.add_link(link_doctype="Note", link_name=note.name, autosave=True) + comm.add_link(link_doctype="Note", link_name=note.name, autosave=True) + comm = frappe.get_doc("Communication", comm.name) self.assertNotEqual(2, len(comm.timeline_links)) @@ -140,42 +135,35 @@ class TestCommunication(unittest.TestCase): def test_get_communication_data(self): from frappe.desk.form.load import get_communication_data - todo = frappe.get_doc({ - "doctype": "ToDo", - "description": "Test ToDo" + note = frappe.get_doc({ + "doctype": "Note", + "title": "get communication data", + "content": "get communication data" }).insert(ignore_permissions=True) - comm_todo_1 = frappe.get_doc({ + comm_note_1 = frappe.get_doc({ "doctype": "Communication", "communication_type": "Communication", "content": "Test Get Communication Data 1", - "communication_medium": "Email", - "timeline_links": [ - { - "link_doctype": "ToDo", - "link_name": todo.name - } - ] + "communication_medium": "Email" }).insert(ignore_permissions=True) - comm_todo_2 = frappe.get_doc({ + comm_note_1.add_link(link_doctype="Note", link_name=note.name, autosave=True) + + comm_note_2 = frappe.get_doc({ "doctype": "Communication", "communication_type": "Communication", "content": "Test Get Communication Data 2", - "communication_medium": "Email", - "timeline_links": [ - { - "link_doctype": "ToDo", - "link_name": todo.name - } - ] + "communication_medium": "Email" }).insert(ignore_permissions=True) - comms = get_communication_data("ToDo", todo.name, as_dict=True) + comm_note_2.add_link(link_doctype="Note", link_name=note.name, autosave=True) + + comms = get_communication_data("Note", note.name, as_dict=True) data = [] for comm in comms: data.append(comm.name) - self.assertIn(comm_todo_1.name, data) - self.assertIn(comm_todo_2.name, data) \ No newline at end of file + self.assertIn(comm_note_1.name, data) + self.assertIn(comm_note_2.name, data) \ No newline at end of file From 81d314bf102011e6cb0f406f4fd3f4fa1a345cee Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Mon, 20 May 2019 14:40:20 +0530 Subject: [PATCH 49/60] fix: typo fixes --- frappe/core/doctype/communication/communication.py | 1 - frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index aa70024eca..c164f7f8e0 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -238,7 +238,6 @@ class Communication(Document): # Timeline Links def set_timeline_links(self): contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc]) - print(contacts) for contact_name in contacts: self.add_link('Contact', contact_name) diff --git a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py index 624be6ae40..873988c7f3 100644 --- a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py +++ b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py @@ -40,7 +40,7 @@ def execute_query(values): (`idx`, `name`, `parentfield`, `parenttype`, `parent`, `link_doctype`, `link_name`, `creation`, `modified`, `modified_by`) VALUES ({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}) - """.format(values[0], values[1], values[2], values[3], values[4], values[5], values[6], values[7], values[8], values[9]) + """.format(values[0], values[1], values[2], values[3], values[4], values[5], values[6], values[7], values[8], values[9])) except Exception as e: values[1] = frappe.generate_hash(length=10) execute_query(values) From 90c62d0c99e2f1a0ff0eaa614fcb73d2c6a33588 Mon Sep 17 00:00:00 2001 From: Sachin Mane Date: Mon, 20 May 2019 18:55:33 +0530 Subject: [PATCH 50/60] .get() does not work in JavaScript. --- frappe/public/js/frappe/request.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/request.js b/frappe/public/js/frappe/request.js index db0b4bbe46..9a3fe3699b 100644 --- a/frappe/public/js/frappe/request.js +++ b/frappe/public/js/frappe/request.js @@ -198,7 +198,7 @@ frappe.request.call = function(opts) { headers: Object.assign({ "X-Frappe-CSRF-Token": frappe.csrf_token, "Accept": "application/json", - "X-Frappe-CMD": opts.get('args', {}).get('cmd', '') + "X-Frappe-CMD": (opts.args && opts.args.cmd || '') || '' }, opts.headers), cache: false }; From 9c4e1eea5c24fe3b78ad309eeb4e3c207aedef31 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 20 May 2019 18:58:32 +0530 Subject: [PATCH 51/60] fix(print): semantic nested lists for text editor --- frappe/public/build.json | 5 ++ .../js/frappe/form/controls/text_editor.js | 75 ++++++++++++++++++- frappe/public/less/desk.less | 1 + frappe/public/less/print.less | 15 ++++ .../print_formats/standard_macros.html | 4 +- frappe/www/printview.html | 5 +- 6 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 frappe/public/less/print.less diff --git a/frappe/public/build.json b/frappe/public/build.json index cd8f04b2c9..a279519d72 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -137,6 +137,11 @@ "public/css/desk-rtl.css", "public/css/report-rtl.css" ], + "css/printview.css": [ + "public/css/bootstrap.css", + "public/css/font-awesome.css", + "public/less/print.less" + ], "concat:js/libs.min.js": [ "public/js/lib/awesomplete/awesomplete.min.js", "public/js/lib/Sortable.min.js", diff --git a/frappe/public/js/frappe/form/controls/text_editor.js b/frappe/public/js/frappe/form/controls/text_editor.js index ef6afde262..e616b9ef3e 100644 --- a/frappe/public/js/frappe/form/controls/text_editor.js +++ b/frappe/public/js/frappe/form/controls/text_editor.js @@ -57,7 +57,7 @@ Quill.register(DirectionStyle, true); frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ make_wrapper() { this._super(); - this.$wrapper.find(".like-disabled-input").addClass("ql-editor"); + this.$wrapper.find(".like-disabled-input").addClass('text-editor-print'); }, make_input() { @@ -198,7 +198,78 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ let $ul = $('
    ').append($children); $parent.replaceWith($ul); }); - value = $value.html(); + value = this.convertLists($value.html()); return value; + }, + + // hack + // https://github.com/quilljs/quill/issues/979 + convertLists(richtext) { + const tempEl = window.document.createElement('div'); + tempEl.setAttribute('style', 'display: none;'); + tempEl.innerHTML = richtext; + const startLi = '::startli::'; + const endLi = '::endli::'; + + ['ul','ol'].forEach((type) => { + const startTag = `::start${type}::`; + const endTag = `::end${type}::`; + + // Grab each list, and work on it in turn + Array.from(tempEl.querySelectorAll(type)).forEach((outerListEl) => { + const listChildren = Array.from(outerListEl.children).filter((el) => el.tagName === 'LI'); + + let lastLiLevel = 0; + let currentLiLevel = 0; + let difference = 0; + + // Now work through each li in this list + for (let i = 0; i < listChildren.length; i++) { + const currentLi = listChildren[i]; + lastLiLevel = currentLiLevel; + currentLiLevel = this.getListLevel(currentLi); + difference = currentLiLevel - lastLiLevel; + + // we only need to add tags if the level is changing + if (difference > 0) { + currentLi.before((startLi + startTag).repeat(difference)); + } else if (difference < 0) { + currentLi.before((endTag + endLi).repeat(-difference)); + } + + if (i === listChildren.length - 1) { + // last li, account for the fact that it might not be at level 0 + currentLi.after((endTag + endLi).repeat(currentLiLevel)); + } + } + }); + }); + + // Get the content in the element and replace the temporary tags with new ones + let newContent = tempEl.innerHTML; + + newContent = newContent.replace(/::startul::/g, '
      '); + newContent = newContent.replace(/::endul::/g, '
    '); + newContent = newContent.replace(/::startol::/g, '
      '); + newContent = newContent.replace(/::endol::/g, '
    '); + newContent = newContent.replace(/::startli::/g, '
  • '); + newContent = newContent.replace(/::endli::/g, '
  • '); + + // remove quill classes + newContent = newContent.replace(/data-list=.bullet./g, ''); + newContent = newContent.replace(/class=.ql-indent-../g, ''); + + // ul/ol should not be inside another li + newContent = newContent.replace(/<\/li>
    • /g, '
        '); + newContent = newContent.replace(/<\/li>
        1. /g, '
            '); + tempEl.remove(); + + return newContent; + }, + + getListLevel(el) { + const className = el.className || '0'; + return +className.replace(/[^\d]/g, ''); } + }); diff --git a/frappe/public/less/desk.less b/frappe/public/less/desk.less index 5d043babc1..d9af075984 100644 --- a/frappe/public/less/desk.less +++ b/frappe/public/less/desk.less @@ -2,6 +2,7 @@ @import "mixins.less"; @import "common.less"; @import "quill.less"; +@import "print.less"; .nav-pills a, .nav-pills a:hover { border-bottom: none; diff --git a/frappe/public/less/print.less b/frappe/public/less/print.less new file mode 100644 index 0000000000..203b1237c5 --- /dev/null +++ b/frappe/public/less/print.less @@ -0,0 +1,15 @@ +.text-editor-print { + ul li { + list-style-type: none; + padding-left: 1.5em; + } + + ul li:before { + content: '\2022'; + margin-left: -1.5em; + margin-right: 0.3em; + text-align: right; + white-space: nowrap; + width: 1.2em; + } +} diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html index ea667704af..2d8035ac93 100644 --- a/frappe/templates/print_formats/standard_macros.html +++ b/frappe/templates/print_formats/standard_macros.html @@ -97,8 +97,10 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}" {%- if df.fieldtype=="Code" %}
            {{ doc.get(df.fieldname) }}
            {% else -%} + {%- if df.fieldtype=="Text Editor" -%}
            {%- endif -%} {{ doc.get_formatted(df.fieldname, parent_doc or doc, translated=df.translatable) }} - {% endif -%} + {%- if df.fieldtype=="Text Editor" -%}
            {%- endif -%} + {% endif -%} {%- endif -%} {%- endmacro -%} diff --git a/frappe/www/printview.html b/frappe/www/printview.html index 2953ff2c90..b7077b5a12 100644 --- a/frappe/www/printview.html +++ b/frappe/www/printview.html @@ -5,10 +5,7 @@ {{ title }} - - + {%- if has_rtl -%} {%- endif -%} From 59c945bb84cdeea04eb77c1925d906f54f09d8db Mon Sep 17 00:00:00 2001 From: Himanshu Date: Tue, 21 May 2019 00:00:57 +0530 Subject: [PATCH 52/60] fix(communication): Make optimizations to the patch * use batches to move Timeline and Links to Timeline Links doctype --- .../move_timeline_links_to_dynamic_links.py | 60 +++++++++---------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py index 873988c7f3..ce544d658c 100644 --- a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py +++ b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py @@ -6,41 +6,39 @@ def execute(): frappe.reload_doc('core', 'doctype', 'communication') communications = frappe.db.sql(""" - SELECT - `tabCommunication`.name, `tabCommunication`.creation, `tabCommunication`.modified, - `tabCommunication`.modified_by,`tabCommunication`.timeline_doctype, `tabCommunication`.timeline_name, - `tabCommunication`.link_doctype, `tabCommunication`.link_name - FROM `tabCommunication` - WHERE `tabCommunication`.communication_medium='Email' - """, as_dict=True) + SELECT + `tabCommunication`.name, `tabCommunication`.creation, `tabCommunication`.modified, + `tabCommunication`.modified_by,`tabCommunication`.timeline_doctype, `tabCommunication`.timeline_name, + `tabCommunication`.link_doctype, `tabCommunication`.link_name + FROM `tabCommunication` + WHERE `tabCommunication`.communication_medium='Email' + """, as_dict=True) + + name = 10000000000001234567 + values = [] for count, communication in enumerate(communications): counter = 1 if communication.timeline_doctype and communication.timeline_name: - values = [ - counter, frappe.generate_hash(length=10), 'timeline_links', 'Communication', communication.name, - communication.timeline_doctype, communication.timeline_name, communication.creation, communication.modified, - communication.modified_by - ] - execute_query(values) + name += 1 + values.append("""({0}, "{1}", "timeline_links", "Communication", "{2}", "{3}", "{4}", "{5}", "{6}", "{7}")""".format( + counter, str(name), communication.name, communication.timeline_doctype, + communication.timeline_name, communication.creation, communication.modified, communication.modified_by + )) counter += 1 - if communication.link_doctype and communication.link_name: - values = [ - counter, frappe.generate_hash(length=10), 'timeline_links', 'Communication', communication.name, - communication.link_doctype, communication.link_name, communication.creation, communication.modified, - communication.modified_by - ] - execute_query(values) + name += 1 + values.append("""({0}, "{1}", "timeline_links", "Communication", "{2}", "{3}", "{4}", "{5}", "{6}", "{7}")""".format( + counter, str(name), communication.name, communication.link_doctype, + communication.link_name, communication.creation, communication.modified, communication.modified_by + )) -def execute_query(values): - try: - frappe.db.sql(""" - INSERT INTO `tabDynamic Link` - (`idx`, `name`, `parentfield`, `parenttype`, `parent`, `link_doctype`, `link_name`, `creation`, - `modified`, `modified_by`) - VALUES ({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}) - """.format(values[0], values[1], values[2], values[3], values[4], values[5], values[6], values[7], values[8], values[9])) - except Exception as e: - values[1] = frappe.generate_hash(length=10) - execute_query(values) + if count % 10000 == 0 or count == len(communications) - 1: + frappe.db.sql(""" + INSERT INTO `tabDynamic Link` + (`idx`, `name`, `parentfield`, `parenttype`, `parent`, `link_doctype`, `link_name`, `creation`, + `modified`, `modified_by`) + VALUES {0} + """.format(", ".join([d for d in values]))) + + values = [] From 105f9198c614bd22fae756c3f310732c9c6b6590 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 21 May 2019 11:25:13 +0530 Subject: [PATCH 53/60] fix(patch): move_timeline_links_to_dynamic_link.py handle empty set --- frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py index ce544d658c..41ca11a0a4 100644 --- a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py +++ b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py @@ -33,7 +33,7 @@ def execute(): communication.link_name, communication.creation, communication.modified, communication.modified_by )) - if count % 10000 == 0 or count == len(communications) - 1: + if values and (count % 10000 == 0 or count == len(communications) - 1): frappe.db.sql(""" INSERT INTO `tabDynamic Link` (`idx`, `name`, `parentfield`, `parenttype`, `parent`, `link_doctype`, `link_name`, `creation`, From 7d7b229438f2eb67173629fcf4c7c9e9d5e025f4 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 21 May 2019 12:03:58 +0530 Subject: [PATCH 54/60] perf: Optimize get_communications Split query with OR condition into two subqueries with UNION --- frappe/desk/form/load.py | 71 +++++++++++-------- .../move_timeline_links_to_dynamic_links.py | 13 ++-- 2 files changed, 47 insertions(+), 37 deletions(-) diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 2985a8858e..693efe610c 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -161,49 +161,58 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields= '''Returns list of communications for a given document''' if not fields: fields = ''' - `tabCommunication`.name, `tabCommunication`.communication_type, `tabCommunication`.communication_medium, - `tabCommunication`.comment_type, `tabCommunication`.communication_date, `tabCommunication`.content, - `tabCommunication`.sender, `tabCommunication`.sender_full_name, `tabCommunication`.cc, `tabCommunication`.bcc, - `tabCommunication`.creation, `tabCommunication`.subject, `tabCommunication`.delivery_status, - `tabCommunication`._liked_by, `tabCommunication`.reference_doctype, `tabCommunication`.reference_name, - `tabCommunication`.read_by_recipient, `tabCommunication`.rating + C.name, C.communication_type, C.communication_medium, + C.comment_type, C.communication_date, C.content, + C.sender, C.sender_full_name, C.cc, C.bcc, + C.creation, C.subject, C.delivery_status, + C._liked_by, C.reference_doctype, C.reference_name, + C.read_by_recipient, C.rating ''' - conditions = ''' - `tabCommunication`.communication_type in ('Communication', 'Feedback') - and ( - (`tabCommunication`.reference_doctype=%(doctype)s and `tabCommunication`.reference_name=%(name)s) - or ( - (`tabDynamic Link`.link_doctype=%(doctype)s and `tabDynamic Link`.link_name=%(name)s) - and (`tabCommunication`.communication_type='Communication') - ) - ) - ''' - + conditions = '' if after: # find after a particular date conditions += ''' - and `tabCommunication`.creation > {0} + AND C.creation > {0} '''.format(after) if doctype=='User': conditions += ''' - and not (`tabCommunication`.reference_doctype='User' and `tabCommunication`.communication_type='Communication') + AND NOT (C.reference_doctype='User' AND C.communication_type='Communication') ''' + # communications linked to reference_doctype + part1 = ''' + SELECT {fields} + FROM `tabCommunication` as C + WHERE C.communication_type IN ('Communication', 'Feedback') + AND (C.reference_doctype = %(doctype)s AND C.reference_name = %(name)s) + {conditions} + '''.format(fields=fields, conditions=conditions) + + # communications linked in Timeline Links + part2 = ''' + SELECT {fields} + FROM `tabCommunication` as C + INNER JOIN `tabDynamic Link` ON C.name=`tabDynamic Link`.parent + WHERE C.communication_type IN ('Communication', 'Feedback') + AND `tabDynamic Link`.link_doctype = %(doctype)s AND `tabDynamic Link`.link_name = %(name)s + {conditions} + '''.format(fields=fields, conditions=conditions) + communications = frappe.db.sql(''' - select distinct {fields} - from `tabCommunication` - inner join `tabDynamic Link` - on `tabCommunication`.name=`tabDynamic Link`.parent - where {conditions} {group_by} - order by `tabCommunication`.creation desc - limit %(limit)s offset %(start)s'''.format(fields = fields, conditions=conditions, group_by=group_by or ""),{ - "doctype": doctype, - "name": name, - "start": frappe.utils.cint(start), - "limit": limit - }, as_dict=as_dict) + SELECT * + FROM (({part1}) UNION ({part2})) AS combined + {group_by} + ORDER BY combined.creation DESC + LIMIT %(limit)s + OFFSET %(start)s + '''.format(part1=part1, part2=part2, group_by=(group_by or '')), dict( + doctype=doctype, + name=name, + start=frappe.utils.cint(start), + limit=limit + ), as_dict=as_dict) return communications diff --git a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py index ce544d658c..5dee253fa1 100644 --- a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py +++ b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py @@ -34,11 +34,12 @@ def execute(): )) if count % 10000 == 0 or count == len(communications) - 1: - frappe.db.sql(""" - INSERT INTO `tabDynamic Link` - (`idx`, `name`, `parentfield`, `parenttype`, `parent`, `link_doctype`, `link_name`, `creation`, - `modified`, `modified_by`) - VALUES {0} - """.format(", ".join([d for d in values]))) + if values: + frappe.db.sql(""" + INSERT INTO `tabDynamic Link` + (`idx`, `name`, `parentfield`, `parenttype`, `parent`, `link_doctype`, `link_name`, `creation`, + `modified`, `modified_by`) + VALUES {0} + """.format(", ".join([d for d in values]))) values = [] From 1fe6f7ceaaa747b95891701595a6fd6f6c18eac7 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 21 May 2019 13:18:51 +0530 Subject: [PATCH 55/60] fix: Show "1 point" instead of "1 points" (#7489) * fix: Show "1 point" instead of "1 points" * fix: Make text translatable * fix: Format string twice to avoid duplicate code - First formatting to add points text - Second formatting to add variables aftter translation * fix(energy point): Alert messages translation --- .../energy_point_log/energy_point_log.py | 47 ++++++++++++------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/frappe/social/doctype/energy_point_log/energy_point_log.py b/frappe/social/doctype/energy_point_log/energy_point_log.py index 42e12c0374..c94c1bd371 100644 --- a/frappe/social/doctype/energy_point_log/energy_point_log.py +++ b/frappe/social/doctype/energy_point_log/energy_point_log.py @@ -31,36 +31,51 @@ class EnergyPointLog(Document): frappe.publish_realtime('update_points', after_commit=True) def get_alert_dict(doc): - alert_dict = frappe._dict({ - 'message': '', - 'indicator': 'green' - }) + alert_dict = frappe._dict() owner_name = get_fullname(doc.owner) doc_link = frappe.get_desk_link(doc.reference_doctype, doc.reference_name) - points = frappe.bold(doc.points) + points = doc.points + bold_points = frappe.bold(doc.points) if doc.type == 'Auto': - alert_dict.message=_('You gained {} points').format(points) + if points == 1: + message = _('You gained {0} point') + else: + message = _('You gained {0} points') + alert_dict.message = message.format(bold_points) + alert_dict.indicator = 'green' elif doc.type == 'Appreciation': - alert_dict.message = _('{} appreciated your work on {} with {} points').format( + if points == 1: + message = _('{0} appreciated your work on {1} with {2} point') + else: + message = _('{0} appreciated your work on {1} with {2} points') + alert_dict.message = message.format( owner_name, doc_link, - points + bold_points ) + alert_dict.indicator = 'green' elif doc.type == 'Criticism': - alert_dict.message = _('{} criticized your work on {} with {} points').format( + if points == 1: + message = _('{0} criticized your work on {1} with {2} point') + else: + message = _('{0} criticized your work on {1} with {2} points') + + alert_dict.message = message.format( owner_name, doc_link, - points + bold_points ) alert_dict.indicator = 'red' elif doc.type == 'Revert': - alert_dict.message = _('{} reverted your points on {}').format( + if points == 1: + message = _('{0} reverted your point on {1}') + else: + message = _('{0} reverted your points on {1}') + alert_dict.message = message.format( owner_name, doc_link, ) alert_dict.indicator = 'red' - else: - alert_dict = {} return alert_dict @@ -135,7 +150,7 @@ def get_user_energy_and_review_points(user=None, from_date=None, as_dict=True): {conditions} GROUP BY `user` ORDER BY `energy_points` DESC - """.format(conditions=conditions), values=values or (), as_dict=1) + """.format(conditions=conditions), values=tuple(values), as_dict=1) if not as_dict: return points_list @@ -245,7 +260,7 @@ def send_summary(timespan): def get_footer_message(timespan): if timespan == 'Monthly': - return _("Stats based on last month's performance (from {} to {})") + return _("Stats based on last month's performance (from {0} to {1})") else: - return _("Stats based on last week's performance (from {} to {})") + return _("Stats based on last week's performance (from {0} to {1})") From d7527fab79a77c77b53a083656a77b4213769a5f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 21 May 2019 14:49:59 +0530 Subject: [PATCH 56/60] fix: Allow eligible fields to be updated from notifications (#7518) --- frappe/email/doctype/notification/notification.py | 11 ++++++++--- .../doctype/ldap_settings/ldap_settings.py | 3 +++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index 61906f585e..f53d7845c5 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -126,9 +126,14 @@ def get_context(context): self.send_a_slack_msg(doc, context) if self.set_property_after_alert: - frappe.db.set_value(doc.doctype, doc.name, self.set_property_after_alert, - self.property_value, update_modified = False) - doc.set(self.set_property_after_alert, self.property_value) + allow_update = True + if doc.docstatus == 1 and not doc.meta.get_field(self.set_property_after_alert).allow_on_submit: + allow_update = False + + if allow_update: + frappe.db.set_value(doc.doctype, doc.name, self.set_property_after_alert, + self.property_value, update_modified = False) + doc.set(self.set_property_after_alert, self.property_value) def send_an_email(self, doc, context): from email.utils import formataddr diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py index 0a4d871be8..e48619bfdd 100644 --- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py +++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py @@ -10,6 +10,9 @@ from frappe.model.document import Document class LDAPSettings(Document): def validate(self): + if not self.enabled: + return + if not self.flags.ignore_mandatory: if self.ldap_search_string.endswith("={0}"): if self.enabled: From 0e7123d14146838d15c00076eeb7043ff4406f71 Mon Sep 17 00:00:00 2001 From: Himanshu Date: Tue, 21 May 2019 14:50:49 +0530 Subject: [PATCH 57/60] fix(Addresses and Contacts Report): Frappe throw when no record present (#7504) --- .../report/addresses_and_contacts/addresses_and_contacts.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py b/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py index 0907476b30..f73858c8ab 100644 --- a/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py +++ b/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals from six import iteritems import frappe - +from frappe import _ field_map = { "Contact": [ "first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact" ], @@ -94,6 +94,9 @@ def get_reference_details(reference_doctype, doctype, reference_list, reference_ for d in records: temp_records.append(d[1:]) + if not reference_list: + frappe.throw(_("No records present in {0}".format(reference_doctype))) + reference_details[reference_list[0]][frappe.scrub(doctype)] = temp_records return reference_details From 6655e45273d8c050f5d9eebe7141274ba9484612 Mon Sep 17 00:00:00 2001 From: Prssanna Desai Date: Tue, 21 May 2019 14:54:11 +0530 Subject: [PATCH 58/60] fix: Increase font size in Module View (#7513) --- frappe/public/js/frappe/views/components/ModuleLinkItem.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/public/js/frappe/views/components/ModuleLinkItem.vue b/frappe/public/js/frappe/views/components/ModuleLinkItem.vue index eccd0b0c33..946bc8c4ac 100644 --- a/frappe/public/js/frappe/views/components/ModuleLinkItem.vue +++ b/frappe/public/js/frappe/views/components/ModuleLinkItem.vue @@ -92,6 +92,7 @@ a:hover, a:focus { .link-content { flex: 1; + font-size: 1.1em; } .popover { From 2b14c956447629b61635393319ddeac360b93802 Mon Sep 17 00:00:00 2001 From: Himanshu Date: Tue, 21 May 2019 20:48:55 +0530 Subject: [PATCH 59/60] fix(Communication): New child doctype for Communication Timeline Links (#7524) * fix: separate child doctype for communication * fix: reload doc before running the patches * fix: update_timeline_doc * fix: manipulatetimeline fields in Comm Links * fix: test cases * fix: reload comm_link before communication * patch: remove redundant patch --- .../doctype/communication/communication.json | 4 +- .../doctype/communication/communication.py | 1 + .../communication/test_communication.py | 4 ++ .../doctype/communication_link/__init__.py | 0 .../communication_link.json | 47 +++++++++++++++++++ .../communication_link/communication_link.py | 10 ++++ frappe/desk/doctype/event/event.py | 8 ++-- frappe/desk/doctype/todo/todo.py | 2 +- frappe/desk/form/load.py | 4 +- .../email_account/test_email_account.py | 2 +- frappe/model/delete_doc.py | 4 +- frappe/model/document.py | 16 ++----- frappe/patches.txt | 2 + .../move_timeline_links_to_dynamic_links.py | 8 ++-- 14 files changed, 84 insertions(+), 28 deletions(-) create mode 100644 frappe/core/doctype/communication_link/__init__.py create mode 100644 frappe/core/doctype/communication_link/communication_link.json create mode 100644 frappe/core/doctype/communication_link/communication_link.py diff --git a/frappe/core/doctype/communication/communication.json b/frappe/core/doctype/communication/communication.json index 68a0adb634..34d8d29bdb 100644 --- a/frappe/core/doctype/communication/communication.json +++ b/frappe/core/doctype/communication/communication.json @@ -377,13 +377,13 @@ "fieldname": "timeline_links", "fieldtype": "Table", "label": "Timeline Links", - "options": "Dynamic Link", + "options": "Communication Link", "permlevel": 2 } ], "icon": "fa fa-comment", "idx": 1, - "modified": "2019-05-20 14:14:01.514493", + "modified": "2019-05-21 09:48:24.892143", "modified_by": "Administrator", "module": "Core", "name": "Communication", diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index 0fb2efc7ec..7ed4aea4b5 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -286,6 +286,7 @@ def on_doctype_update(): """Add indexes in `tabCommunication`""" frappe.db.add_index("Communication", ["reference_doctype", "reference_name"]) frappe.db.add_index("Communication", ["status", "communication_type"]) + frappe.db.add_index("Communication Link", ["link_doctype", "link_name"]) def has_permission(doc, ptype, user): if ptype=="read": diff --git a/frappe/core/doctype/communication/test_communication.py b/frappe/core/doctype/communication/test_communication.py index a783157dc0..21756280a9 100644 --- a/frappe/core/doctype/communication/test_communication.py +++ b/frappe/core/doctype/communication/test_communication.py @@ -73,6 +73,8 @@ class TestCommunication(unittest.TestCase): self.assertRaises(frappe.CircularLinkingError, a.save) def test_deduplication_timeline_links(self): + frappe.delete_doc_if_exists("Note", "deduplication timeline links") + note = frappe.get_doc({ "doctype": "Note", "title": "deduplication timeline links", @@ -135,6 +137,8 @@ class TestCommunication(unittest.TestCase): def test_get_communication_data(self): from frappe.desk.form.load import get_communication_data + frappe.delete_doc_if_exists("Note", "get communication data") + note = frappe.get_doc({ "doctype": "Note", "title": "get communication data", diff --git a/frappe/core/doctype/communication_link/__init__.py b/frappe/core/doctype/communication_link/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/communication_link/communication_link.json b/frappe/core/doctype/communication_link/communication_link.json new file mode 100644 index 0000000000..1dd051bed2 --- /dev/null +++ b/frappe/core/doctype/communication_link/communication_link.json @@ -0,0 +1,47 @@ +{ + "creation": "2019-05-21 09:47:23.043960", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "link_doctype", + "link_name", + "link_title" + ], + "fields": [ + { + "fieldname": "link_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Link DocType", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "link_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Link Name", + "options": "link_doctype", + "reqd": 1 + }, + { + "fieldname": "link_title", + "fieldtype": "Read Only", + "in_list_view": 1, + "label": "Link Title", + "read_only": 1 + } + ], + "istable": 1, + "modified": "2019-05-21 09:47:23.043960", + "modified_by": "Administrator", + "module": "Core", + "name": "Communication Link", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/communication_link/communication_link.py b/frappe/core/doctype/communication_link/communication_link.py new file mode 100644 index 0000000000..0328f2b86a --- /dev/null +++ b/frappe/core/doctype/communication_link/communication_link.py @@ -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 +# import frappe +from frappe.model.document import Document + +class CommunicationLink(Document): + pass diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py index d99cc64436..1ad57246a6 100644 --- a/frappe/desk/doctype/event/event.py +++ b/frappe/desk/doctype/event/event.py @@ -46,8 +46,8 @@ class Event(Document): comms = frappe.get_list("Communication", filters=[ ["Communication", "reference_doctype", "=", self.doctype], ["Communication", "reference_name", "=", self.name], - ["Dynamic Link", "link_doctype", "=", participant.reference_doctype], - ["Dynamic Link", "link_name", "=", participant.reference_docname] + ["Communication Link", "link_doctype", "=", participant.reference_doctype], + ["Communication Link", "link_name", "=", participant.reference_docname] ], fields=["name"]) if comms: @@ -85,8 +85,8 @@ def delete_communication(event, reference_doctype, reference_docname): comms = frappe.get_list("Communication", filters=[ ["Communication", "reference_doctype", "=", event.get("doctype")], ["Communication", "reference_name", "=", event.get("name")], - ["Dynamic Link", "link_doctype", "=", deleted_participant.reference_doctype], - ["Dynamic Link", "link_name", "=", deleted_participant.reference_docname] + ["Communication Link", "link_doctype", "=", deleted_participant.reference_doctype], + ["Communication Link", "link_name", "=", deleted_participant.reference_docname] ], fields=["name"]) if comms: diff --git a/frappe/desk/doctype/todo/todo.py b/frappe/desk/doctype/todo/todo.py index 16530c23dc..5d04f412c0 100644 --- a/frappe/desk/doctype/todo/todo.py +++ b/frappe/desk/doctype/todo/todo.py @@ -44,7 +44,7 @@ class ToDo(Document): def on_trash(self): # unlink todo from linked comments frappe.db.sql(""" - delete from `tabDynamic Link` + delete from `tabCommunication Link` where link_doctype=%(doctype)s and link_name=%(name)s""", { "doctype": self.doctype, "name": self.name }) diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 693efe610c..1538c07221 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -194,9 +194,9 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields= part2 = ''' SELECT {fields} FROM `tabCommunication` as C - INNER JOIN `tabDynamic Link` ON C.name=`tabDynamic Link`.parent + INNER JOIN `tabCommunication Link` ON C.name=`tabCommunication Link`.parent WHERE C.communication_type IN ('Communication', 'Feedback') - AND `tabDynamic Link`.link_doctype = %(doctype)s AND `tabDynamic Link`.link_name = %(name)s + AND `tabCommunication Link`.link_doctype = %(doctype)s AND `tabCommunication Link`.link_name = %(name)s {conditions} '''.format(fields=fields, conditions=conditions) diff --git a/frappe/email/doctype/email_account/test_email_account.py b/frappe/email/doctype/email_account/test_email_account.py index ac16f12477..29b54d7f8b 100644 --- a/frappe/email/doctype/email_account/test_email_account.py +++ b/frappe/email/doctype/email_account/test_email_account.py @@ -204,4 +204,4 @@ def cleanup(sender=None): names = frappe.get_list("Communication", filters=filters, fields=["name"]) for name in names: frappe.delete_doc_if_exists("Communication", name.name) - frappe.delete_doc_if_exists("Dynamic Link", {"parent": name.name}) \ No newline at end of file + frappe.delete_doc_if_exists("Communication Link", {"parent": name.name}) \ No newline at end of file diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 8a557f3b3f..cdf099e50e 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -301,8 +301,8 @@ def clear_references(doctype, reference_doctype, reference_name, (reference_doctype, reference_name)) def clear_timeline_references(link_doctype, link_name): - frappe.db.sql("""delete from `tabDynamic Link` - where `tabDynamic Link`.link_doctype='{0}' and `tabDynamic Link`.link_name='{1}'""".format(link_doctype, link_name)) # nosec + frappe.db.sql("""delete from `tabCommunication Link` + where `tabCommunication Link`.link_doctype='{0}' and `tabCommunication Link`.link_name='{1}'""".format(link_doctype, link_name)) # nosec def insert_feed(doc): from frappe.utils import get_fullname diff --git a/frappe/model/document.py b/frappe/model/document.py index e8add2d619..3ed3118571 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -1195,18 +1195,10 @@ class Document(BaseDocument): return # update timeline doc in communication if it is different than current timeline doc - frappe.db.sql("""update `tabCommunication` - set timeline_doctype=%(timeline_doctype)s, timeline_name=%(timeline_name)s - where - reference_doctype=%(doctype)s and reference_name=%(name)s - and (timeline_doctype is null or timeline_doctype != %(timeline_doctype)s - or timeline_name is null or timeline_name != %(timeline_name)s)""", - { - "doctype": self.doctype, - "name": self.name, - "timeline_doctype": timeline_doctype, - "timeline_name": timeline_name - }) + communication = frappe.get_doc("Communication", {"reference_doctype": self.doctype, "reference_name": self.name}) + if communication.communication_medium == "Email": + # duplicate entries will be handled by deduplicate links in communication + communication.add_link(link_doctype=timeline_doctype, link_name=timeline_name, autosave=True) def queue_action(self, action, **kwargs): '''Run an action in background. If the action has an inner function, diff --git a/frappe/patches.txt b/frappe/patches.txt index 911e793687..82a33920f3 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -241,4 +241,6 @@ frappe.patches.v12_0.reset_home_settings frappe.patches.v12_0.update_print_format_type frappe.patches.v11_0.remove_doctype_user_permissions_for_page_and_report #2019-05-01 frappe.patches.v12_0.remove_feedback_rating +execute:frappe.reload_doc('core', 'doctype', 'communication_link') +execute:frappe.reload_doc('core', 'doctype', 'communication') frappe.patches.v12_0.move_timeline_links_to_dynamic_links \ No newline at end of file diff --git a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py index 41ca11a0a4..8860c7c00d 100644 --- a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py +++ b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py @@ -3,8 +3,6 @@ from __future__ import unicode_literals import frappe def execute(): - frappe.reload_doc('core', 'doctype', 'communication') - communications = frappe.db.sql(""" SELECT `tabCommunication`.name, `tabCommunication`.creation, `tabCommunication`.modified, @@ -14,7 +12,7 @@ def execute(): WHERE `tabCommunication`.communication_medium='Email' """, as_dict=True) - name = 10000000000001234567 + name = 1000000000 values = [] for count, communication in enumerate(communications): @@ -35,10 +33,12 @@ def execute(): if values and (count % 10000 == 0 or count == len(communications) - 1): frappe.db.sql(""" - INSERT INTO `tabDynamic Link` + INSERT INTO `tabCommunication Link` (`idx`, `name`, `parentfield`, `parenttype`, `parent`, `link_doctype`, `link_name`, `creation`, `modified`, `modified_by`) VALUES {0} """.format(", ".join([d for d in values]))) values = [] + + frappe.db.add_index("Communication Link", ["link_doctype", "link_name"]) \ No newline at end of file From 01dc6bee5b662d1b7af9bced547ac37c5f4b9121 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 22 May 2019 13:21:07 +0530 Subject: [PATCH 60/60] fix(delete_fields): handle commit for DDL separately for MariaDB and Postgres --- frappe/model/__init__.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index 3a8868ec2c..40e8616f12 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -111,8 +111,15 @@ def delete_fields(args_dict, delete=0): fields_need_to_delete = set(fields) & set(existing_fields) if not fields_need_to_delete: continue + + if frappe.conf.db_type == 'mariadb': + # mariadb implicitly commits before DDL, make it explicit + frappe.db.commit() + query = "ALTER TABLE `tab%s` " % dt + \ ", ".join(["DROP COLUMN `%s`" % f for f in fields_need_to_delete]) frappe.db.sql(query) - # commit the results to db - frappe.db.commit() + + if frappe.conf.db_type == 'postgres': + # commit the results to db + frappe.db.commit()