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.json b/frappe/core/doctype/communication/communication.json index 3b845964f4..68a0adb634 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", @@ -41,14 +41,11 @@ "user", "column_break_27", "email_template", - "link_doctype", - "link_name", - "timeline_doctype", - "timeline_name", - "timeline_label", "unread_notification_sent", "seen", "_user_tags", + "timeline_links_sections", + "timeline_links", "email_inbox", "message_id", "uid", @@ -204,6 +201,7 @@ "label": "Date" }, { + "default": "0", "fieldname": "read_receipt", "fieldtype": "Check", "label": "Sent Read Receipt", @@ -220,6 +218,7 @@ "read_only": 1 }, { + "default": "0", "fieldname": "read_by_recipient", "fieldtype": "Check", "label": "Read by Recipient", @@ -284,39 +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 - }, - { - "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", @@ -325,6 +291,7 @@ "read_only": 1 }, { + "default": "0", "fieldname": "seen", "fieldtype": "Check", "label": "Seen", @@ -368,6 +335,7 @@ "options": "Open\nSpam\nTrash" }, { + "default": "0", "fieldname": "has_attachment", "fieldtype": "Check", "hidden": 1, @@ -398,11 +366,24 @@ "label": "Email Template", "options": "Email Template", "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "timeline_links_sections", + "fieldtype": "Section Break", + "label": "Timeline Links" + }, + { + "fieldname": "timeline_links", + "fieldtype": "Table", + "label": "Timeline Links", + "options": "Dynamic Link", + "permlevel": 2 } ], "icon": "fa fa-comment", "idx": 1, - "modified": "2019-05-04 15:36:35.818714", + "modified": "2019-05-20 14:14:01.514493", "modified_by": "Administrator", "module": "Core", "name": "Communication", @@ -428,6 +409,18 @@ "role": "System Manager", "share": 1 }, + { + "delete": 1, + "email": 1, + "export": 1, + "permlevel": 2, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, { "delete": 1, "email": 1, @@ -437,6 +430,7 @@ } ], "search_fields": "subject", + "sort_field": "modified", "sort_order": "DESC", "title_field": "subject", "track_changes": 1, diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index 77ccefba71..c164f7f8e0 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -8,11 +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, 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 - +from email.utils import parseaddr from collections import Counter exclude_from_linked_with = True @@ -58,7 +58,10 @@ class Communication(Document): self.set_sender_full_name() validate_email(self) - set_timeline_doc(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: @@ -79,6 +82,7 @@ class Communication(Document): 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) @@ -231,26 +235,66 @@ class Communication(Document): if commit: frappe.db.commit() + # Timeline 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.timeline_links: + t = (l.link_doctype, l.link_name) + if not t in links: + links.append(t) + else: + duplicate = True + + if duplicate: + 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 add_link(self, link_doctype, link_name, autosave=False): + self.append("timeline_links", + { + "link_doctype": link_doctype, + "link_name": link_name + } + ) + + if autosave: + self.save(ignore_permissions=True) + + def get_links(self): + return self.timeline_links + + def remove_link(self, link_doctype, link_name, autosave=False, ignore_permissions=True): + for l in self.timeline_links: + if l.link_doctype == link_doctype and l.link_name == link_name: + self.timeline_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 +314,39 @@ 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_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 0c68f8b118..36377a90f7 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -71,12 +71,9 @@ 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.save(ignore_permissions=True) if isinstance(attachments, string_types): attachments = json.loads(attachments) @@ -557,5 +554,4 @@ def mark_email_as_seen(name=None): frappe.response["type"] = 'binary' frappe.response["filename"] = "imaginary_pixel.png" - frappe.response["filecontent"] = buffered_obj.getvalue() - + frappe.response["filecontent"] = buffered_obj.getvalue() \ No newline at end of file diff --git a/frappe/core/doctype/communication/test_communication.py b/frappe/core/doctype/communication/test_communication.py index 1941ff31cc..a783157dc0 100644 --- a/frappe/core/doctype/communication/test_communication.py +++ b/frappe/core/doctype/communication/test_communication.py @@ -44,28 +44,126 @@ 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, - }).insert() + "content": "This was created to test circular linking: Communication A", + }).insert(ignore_permissions=True) + 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() + }).insert(ignore_permissions=True) + 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() + }).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) + def test_deduplication_timeline_links(self): + 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" + }).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)) + + 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", + "cc": "comm_cc@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 + + note = frappe.get_doc({ + "doctype": "Note", + "title": "get communication data", + "content": "get communication data" + }).insert(ignore_permissions=True) + + comm_note_1 = frappe.get_doc({ + "doctype": "Communication", + "communication_type": "Communication", + "content": "Test Get Communication Data 1", + "communication_medium": "Email" + }).insert(ignore_permissions=True) + + 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" + }).insert(ignore_permissions=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_note_1.name, data) + self.assertIn(comm_note_2.name, data) \ No newline at end of file diff --git a/frappe/core/doctype/dynamic_link/dynamic_link.json b/frappe/core/doctype/dynamic_link/dynamic_link.json index 3689be6a3d..abc47df100 100644 --- a/frappe/core/doctype/dynamic_link/dynamic_link.json +++ b/frappe/core/doctype/dynamic_link/dynamic_link.json @@ -1,125 +1,47 @@ { - "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_name", + "link_title" + ], "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 } - ], - "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-16 19:54:31.400026", + "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/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py index 04f7455e2d..d99cc64436 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", comm.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/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 b45ee6d791..2985a8858e 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -160,36 +160,50 @@ 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`.read_by_recipient, `tabCommunication`.rating + ''' - conditions = '''communication_type = 'Communication' - 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 distinct {fields} from `tabCommunication` + inner 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) return communications @@ -229,4 +243,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..6ef94883f7 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -387,7 +387,7 @@ class EmailAccount(Document): communication._seen = json.dumps(users) 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 5dc74bff8d..8a557f3b3f 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -278,9 +278,8 @@ 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_references('Communication', doctype, name, 'timeline_doctype', 'timeline_name') clear_references('Activity Log', doctype, name) clear_references('Activity Log', doctype, name, 'timeline_doctype', 'timeline_name') @@ -301,6 +300,9 @@ 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): + 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 diff --git a/frappe/patches.txt b/frappe/patches.txt index c0b2a8238f..911e793687 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -241,3 +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 +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..873988c7f3 --- /dev/null +++ b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py @@ -0,0 +1,46 @@ +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, + `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 = [ + 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 = [ + 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 Exception as e: + values[1] = frappe.generate_hash(length=10) + execute_query(values)