From 8381e0048a1bb5d61860af072f8a1ff61fc7ee13 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Mon, 13 May 2019 20:39:49 +0530 Subject: [PATCH] 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)