diff --git a/frappe/__init__.py b/frappe/__init__.py index 877e109281..945280b4f6 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -241,7 +241,7 @@ def msgprint(msg, small=0, raise_exception=0, as_table=False): msg = '' + ''.join([''+''.join(['' % c for c in r])+'' for r in msg]) + '
%s
' if flags.print_messages: - print "Message: " + repr(msg) + print "Message: " + repr(msg).encode("utf-8") message_log.append((small and '__small:' or '')+cstr(msg or '')) _raise_exception() @@ -430,6 +430,9 @@ def has_website_permission(doctype, ptype="read", doc=None, user=None, verbose=F hooks = (get_hooks("has_website_permission") or {}).get(doctype, []) if hooks: + if isinstance(doc, basestring): + doc = get_doc(doctype, doc) + for method in hooks: result = call(get_attr(method), doc=doc, ptype=ptype, user=user, verbose=verbose) # if even a single permission check is Falsy diff --git a/frappe/__version__.py b/frappe/__version__.py index f57c6031c7..40b6256ff9 100644 --- a/frappe/__version__.py +++ b/frappe/__version__.py @@ -1,2 +1,2 @@ from __future__ import unicode_literals -__version__ = "6.0.0-wip" +__version__ = "6.0.0" diff --git a/frappe/auth.py b/frappe/auth.py index d58f22048e..7d450dd78b 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -23,7 +23,7 @@ class HTTPRequest: self.domain = self.domain[4:] if frappe.get_request_header('X-Forwarded-For'): - frappe.local.request_ip = frappe.get_request_header('X-Forwarded-For') + frappe.local.request_ip = (frappe.get_request_header('X-Forwarded-For').split(",")[0]).strip() elif frappe.get_request_header('REMOTE_ADDR'): frappe.local.request_ip = frappe.get_request_header('REMOTE_ADDR') diff --git a/frappe/change_log/v5/v5_4_0.md b/frappe/change_log/v5/v5_4_0.md new file mode 100644 index 0000000000..5bf04bb82d --- /dev/null +++ b/frappe/change_log/v5/v5_4_0.md @@ -0,0 +1 @@ +- Moved Backup Manager and Social Login Keys to the new **Integrations** module diff --git a/frappe/config/desktop.py b/frappe/config/desktop.py index 70b73ffb7e..c5ac49f4d9 100644 --- a/frappe/config/desktop.py +++ b/frappe/config/desktop.py @@ -72,4 +72,10 @@ def get_data(): "type": "module", "system_manager": 1 }, + "Integrations": { + "color": "#36414C", + "icon": "octicon octicon-plug", + "type": "module", + "system_manager": 1 + } } diff --git a/frappe/config/integrations.py b/frappe/config/integrations.py new file mode 100644 index 0000000000..99ee288f5c --- /dev/null +++ b/frappe/config/integrations.py @@ -0,0 +1,23 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return [ + { + "label": _("Documents"), + "icon": "icon-star", + "items": [ + { + "type": "doctype", + "name": "Social Login Keys", + "description": _("Enter keys to enable login via Facebook, Google, GitHub."), + }, + { + "type": "doctype", + "name": "Backup Manager", + "description": _("Manage cloud backups on Dropbox"), + "hide_count": True + } + ] + } + ] diff --git a/frappe/config/setup.py b/frappe/config/setup.py index 0e5fe02046..eed59e885b 100644 --- a/frappe/config/setup.py +++ b/frappe/config/setup.py @@ -208,12 +208,6 @@ def get_data(): "description": _("Install Applications."), "icon": "icon-download" }, - { - "type": "doctype", - "name": "Backup Manager", - "description": _("Manage cloud backups on Dropbox"), - "hide_count": True - }, { "type": "doctype", "name": "Scheduler Log", diff --git a/frappe/config/website.py b/frappe/config/website.py index f118e0f589..8d5f44f6ac 100644 --- a/frappe/config/website.py +++ b/frappe/config/website.py @@ -77,11 +77,6 @@ def get_data(): "type": "doctype", "name": "Website Theme", "description": _("List of themes for Website."), - }, - { - "type": "doctype", - "name": "Social Login Keys", - "description": _("Enter keys to enable login via Facebook, Google, GitHub."), } ] }, diff --git a/frappe/core/doctype/communication/communication.json b/frappe/core/doctype/communication/communication.json index 1c4c33efd8..68439e8d01 100644 --- a/frappe/core/doctype/communication/communication.json +++ b/frappe/core/doctype/communication/communication.json @@ -1,251 +1,267 @@ { - "allow_import": 1, - "autoname": "naming_series:", - "creation": "2013-01-29 10:47:14", - "description": "Keep a track of all communications", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", + "allow_import": 1, + "autoname": "naming_series:", + "creation": "2013-01-29 10:47:14", + "description": "Keep a track of all communications", + "docstatus": 0, + "doctype": "DocType", + "document_type": "Setup", "fields": [ { - "default": "COMM-", - "fieldname": "naming_series", - "fieldtype": "Select", - "hidden": 1, - "label": "Series", - "options": "COMM-", + "default": "COMM-", + "fieldname": "naming_series", + "fieldtype": "Select", + "hidden": 1, + "label": "Series", + "options": "COMM-", "permlevel": 0 - }, - { - "fieldname": "sent_or_received", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Sent or Received", - "options": "Sent\nReceived", - "permlevel": 0, + }, + { + "fieldname": "sent_or_received", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Sent or Received", + "options": "Sent\nReceived", + "permlevel": 0, "reqd": 1 - }, + }, + { + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Open\nReplied\nArchived", + "permlevel": 0, + "precision": "" + }, { - "fieldname": "status", - "fieldtype": "Select", - "label": "Status", - "options": "Open\nReplied\nArchived", - "permlevel": 0, + "description": "Integrations can use this field to set email delivery status", + "fieldname": "delivery_status", + "fieldtype": "Select", + "hidden": 1, + "label": "Delivery Status", + "options": "\nSent\nBounced\nOpened\nMarked As Spam\nRejected\nDelayed\nSoft-Bounced\nClicked\nRecipient Unsubscribed", + "permlevel": 0, "precision": "" - }, + }, { - "fieldname": "subject", - "fieldtype": "Data", - "in_list_view": 0, - "label": "Subject", - "permlevel": 0, + "fieldname": "subject", + "fieldtype": "Data", + "in_list_view": 0, + "label": "Subject", + "permlevel": 0, "reqd": 1 - }, + }, { - "fieldname": "column_break_5", - "fieldtype": "Column Break", - "permlevel": 0, + "fieldname": "column_break_5", + "fieldtype": "Column Break", + "permlevel": 0, "precision": "" - }, + }, { - "fieldname": "reference_doctype", - "fieldtype": "Link", - "label": "Reference DocType", - "options": "DocType", - "permlevel": 0, + "fieldname": "reference_doctype", + "fieldtype": "Link", + "label": "Reference DocType", + "options": "DocType", + "permlevel": 0, "precision": "" - }, + }, { - "fieldname": "reference_name", - "fieldtype": "Dynamic Link", - "label": "Reference Name", - "options": "reference_doctype", - "permlevel": 0, + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "label": "Reference Name", + "options": "reference_doctype", + "permlevel": 0, "precision": "" - }, + }, { - "fieldname": "section_break_8", - "fieldtype": "Section Break", - "permlevel": 0, + "fieldname": "section_break_8", + "fieldtype": "Section Break", + "permlevel": 0, "precision": "" - }, + }, { - "fieldname": "content", - "fieldtype": "Text Editor", - "label": "Content", - "permlevel": 0, - "reqd": 0, + "fieldname": "content", + "fieldtype": "Text Editor", + "label": "Content", + "permlevel": 0, + "reqd": 0, "width": "400" - }, + }, { - "fieldname": "additional_info", - "fieldtype": "Section Break", - "label": "Additional Info", + "fieldname": "additional_info", + "fieldtype": "Section Break", + "label": "Additional Info", "permlevel": 0 - }, + }, { - "fieldname": "recipients", - "fieldtype": "Data", - "label": "Recipients", + "fieldname": "recipients", + "fieldtype": "Data", + "label": "Recipients", "permlevel": 0 - }, + }, { - "fieldname": "sender", - "fieldtype": "Data", - "label": "Sender", + "fieldname": "phone_no", + "fieldtype": "Data", + "label": "Phone No.", "permlevel": 0 - }, + }, { - "fieldname": "sender_full_name", - "fieldtype": "Data", - "label": "Sender Full Name", - "permlevel": 0, + "fieldname": "communication_medium", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Communication Medium", + "options": "\nChat\nPhone\nEmail\nSMS\nVisit\nOther", + "permlevel": 0 + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break", + "permlevel": 0, "precision": "" - }, + }, { - "fieldname": "communication_medium", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Communication Medium", - "options": "\nChat\nPhone\nEmail\nSMS\nVisit\nOther", + "fieldname": "sender", + "fieldtype": "Data", + "label": "Sender", "permlevel": 0 - }, + }, { - "fieldname": "phone_no", - "fieldtype": "Data", - "label": "Phone No.", - "permlevel": 0 - }, + "fieldname": "sender_full_name", + "fieldtype": "Data", + "label": "Sender Full Name", + "permlevel": 0, + "precision": "" + }, { - "fieldname": "section_break2", - "fieldtype": "Section Break", - "options": "simple", + "fieldname": "section_break2", + "fieldtype": "Section Break", + "options": "simple", "permlevel": 0 - }, + }, { - "fieldname": "column_break4", - "fieldtype": "Column Break", - "label": "By", + "fieldname": "column_break4", + "fieldtype": "Column Break", + "label": "By", "permlevel": 0 - }, + }, { - "fieldname": "email_account", - "fieldtype": "Link", - "label": "Email Account", - "options": "Email Account", - "permlevel": 0, + "fieldname": "email_account", + "fieldtype": "Link", + "label": "Email Account", + "options": "Email Account", + "permlevel": 0, "precision": "" - }, - { - "default": "__user", - "fieldname": "user", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "User", - "options": "User", - "permlevel": 0, + }, + { + "default": "__user", + "fieldname": "user", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "User", + "options": "User", + "permlevel": 0, "read_only": 1 - }, + }, { - "fieldname": "column_break5", - "fieldtype": "Column Break", - "label": "On", + "fieldname": "column_break5", + "fieldtype": "Column Break", + "label": "On", "permlevel": 0 - }, + }, { - "default": "Today", - "fieldname": "communication_date", - "fieldtype": "Datetime", - "label": "Date", + "default": "Today", + "fieldname": "communication_date", + "fieldtype": "Datetime", + "label": "Date", "permlevel": 0 - }, - { - "fieldname": "_user_tags", - "fieldtype": "Data", - "hidden": 1, - "label": "User Tags", - "no_copy": 1, - "permlevel": 0, + }, + { + "fieldname": "_user_tags", + "fieldtype": "Data", + "hidden": 1, + "label": "User Tags", + "no_copy": 1, + "permlevel": 0, "print_hide": 1 - }, - { - "default": "0", - "fieldname": "unread_notification_sent", - "fieldtype": "Check", - "label": "Unread Notification Sent", - "permlevel": 0, - "precision": "", + }, + { + "default": "0", + "fieldname": "unread_notification_sent", + "fieldtype": "Check", + "label": "Unread Notification Sent", + "permlevel": 0, + "precision": "", "read_only": 1 } - ], - "icon": "icon-comment", - "idx": 1, - "in_dialog": 0, - "issingle": 0, - "modified": "2015-07-28 16:18:11.664740", - "modified_by": "Administrator", - "module": "Core", - "name": "Communication", - "owner": "Administrator", + ], + "icon": "icon-comment", + "idx": 1, + "in_dialog": 0, + "issingle": 0, + "modified": "2015-07-28 17:18:11.664740", + "modified_by": "Administrator", + "module": "Core", + "name": "Communication", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 1, - "create": 1, - "delete": 1, - "email": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Support Team", - "share": 1, - "submit": 0, + "amend": 0, + "apply_user_permissions": 1, + "create": 1, + "delete": 1, + "email": 1, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Support Team", + "share": 1, + "submit": 0, "write": 1 - }, - { - "amend": 0, - "create": 1, - "delete": 1, - "email": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales Manager", - "share": 1, - "submit": 0, + }, + { + "amend": 0, + "create": 1, + "delete": 1, + "email": 1, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, + "submit": 0, "write": 1 - }, - { - "amend": 0, - "apply_user_permissions": 1, - "create": 1, - "delete": 1, - "email": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "share": 1, - "submit": 0, + }, + { + "amend": 0, + "apply_user_permissions": 1, + "create": 1, + "delete": 1, + "email": 1, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User", + "share": 1, + "submit": 0, "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "submit": 0, + }, + { + "create": 1, + "delete": 1, + "email": 1, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 0, "write": 1 } - ], - "search_fields": "subject", + ], + "search_fields": "subject", "title_field": "subject" -} \ No newline at end of file +} diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 234dea8971..177be05f6d 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -135,6 +135,9 @@ class CustomizeForm(Document): and cint(df.get("precision")) > cint(meta_df[0].get("precision")): update_db = True + elif property == "unique": + update_db = True + self.make_property_setter(property=property, value=df.get(property), property_type=self.docfield_properties[property], fieldname=df.fieldname) diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 1cab39bed2..82cc238068 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -113,7 +113,7 @@ def get_comments(dt, dn, limit=100): communications = frappe.db.sql("""select name, content as comment, sender as comment_by, creation, - communication_medium as comment_type, subject, + communication_medium as comment_type, subject, delivery_status, "Communication" as doctype from tabCommunication where reference_doctype=%s and reference_name=%s diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index e38621829a..b3e6d85673 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -95,8 +95,7 @@ def clear_notifications(user="*"): frappe.cache().delete_keys("notification_count:") else: # delete count for user - for key in frappe.cache().get_keys("notification_count:"): - frappe.cache().hdel(key, user) + frappe.cache().hdel_keys("notification_count:", user) def delete_notification_count_for(doctype): frappe.cache().delete_key("notification_count:" + doctype) diff --git a/frappe/email/bulk.py b/frappe/email/bulk.py index d3c1a1aaf7..5b88bd23e9 100644 --- a/frappe/email/bulk.py +++ b/frappe/email/bulk.py @@ -60,6 +60,9 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc if reference_doctype and reference_name: unsubscribed = [d.email for d in frappe.db.get_all("Email Unsubscribe", "email", {"reference_doctype": reference_doctype, "reference_name": reference_name})] + + unsubscribed += [d.email for d in frappe.db.get_all("Email Unsubscribe", "email", + {"global_unsubscribe": 1})] else: unsubscribed = [] @@ -159,14 +162,19 @@ def unsubscribe(doctype, name, email): if not verify_request(): return - frappe.get_doc({ - "doctype": "Email Unsubscribe", - "email": email, - "reference_doctype": doctype, - "reference_name": name - }).insert(ignore_permissions=True) + try: + frappe.get_doc({ + "doctype": "Email Unsubscribe", + "email": email, + "reference_doctype": doctype, + "reference_name": name + }).insert(ignore_permissions=True) + + except frappe.DuplicateEntryError: + frappe.db.rollback() - frappe.db.commit() + else: + frappe.db.commit() return_unsubscribed_page(email, doctype, name) diff --git a/frappe/email/doctype/email_unsubscribe/email_unsubscribe.json b/frappe/email/doctype/email_unsubscribe/email_unsubscribe.json index a70c363034..8014a44fb9 100644 --- a/frappe/email/doctype/email_unsubscribe/email_unsubscribe.json +++ b/frappe/email/doctype/email_unsubscribe/email_unsubscribe.json @@ -25,7 +25,8 @@ "report_hide": 0, "reqd": 1, "search_index": 0, - "set_only_once": 0 + "set_only_once": 0, + "unique": 0 }, { "allow_on_submit": 0, @@ -43,9 +44,10 @@ "print_hide": 0, "read_only": 0, "report_hide": 0, - "reqd": 1, + "reqd": 0, "search_index": 0, - "set_only_once": 0 + "set_only_once": 0, + "unique": 0 }, { "allow_on_submit": 0, @@ -63,9 +65,30 @@ "print_hide": 0, "read_only": 0, "report_hide": 0, - "reqd": 1, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "fieldname": "global_unsubscribe", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Global Unsubscribe", + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, "search_index": 0, - "set_only_once": 0 + "set_only_once": 0, + "unique": 0 } ], "hide_heading": 0, @@ -75,7 +98,7 @@ "is_submittable": 0, "issingle": 0, "istable": 0, - "modified": "2015-03-18 09:41:20.216319", + "modified": "2015-08-05 06:02:12.805282", "modified_by": "Administrator", "module": "Email", "name": "Email Unsubscribe", @@ -90,6 +113,7 @@ "delete": 1, "email": 1, "export": 1, + "if_owner": 0, "import": 0, "permlevel": 0, "print": 1, diff --git a/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py b/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py index 02d3569988..2f879f98ee 100644 --- a/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py +++ b/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py @@ -8,6 +8,32 @@ from frappe.model.document import Document from frappe import _ class EmailUnsubscribe(Document): + def validate(self): + if not self.global_unsubscribe and not (self.reference_doctype and self.reference_name): + frappe.throw(_("Reference DocType and Reference Name are required"), frappe.MandatoryError) + + if not self.global_unsubscribe and frappe.db.get_value(self.doctype, self.name, "global_unsubscribe"): + frappe.throw(_("Delete this record to allow sending to this email address")) + + if self.global_unsubscribe: + if frappe.get_all("Email Unsubscribe", + filters={"email": self.email, "global_unsubscribe": 1, "name": ["!=", self.name]}): + frappe.throw(_("{0} already unsubscribed").format(self.email), frappe.DuplicateEntryError) + + else: + if frappe.get_all("Email Unsubscribe", + filters={ + "email": self.email, + "reference_doctype": self.reference_doctype, + "reference_name": self.reference_name, + "name": ["!=", self.name] + }): + frappe.throw(_("{0} already unsubscribed for {1} {2}").format( + self.email, self.reference_doctype, self.reference_name), + frappe.DuplicateEntryError) + def on_update(self): - doc = frappe.get_doc(self.reference_doctype, self.reference_name) - doc.add_comment("Label", _("Left this conversation"), comment_by=self.email) + if self.reference_doctype and self.reference_name: + doc = frappe.get_doc(self.reference_doctype, self.reference_name) + doc.add_comment("Label", _("Left this conversation"), comment_by=self.email) + diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index c178cc21ff..48cfdd87c3 100644 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -192,7 +192,7 @@ class EMail: "Date": email.utils.formatdate(), "Reply-To": self.reply_to.encode("utf-8") if self.reply_to else None, "CC": ', '.join(self.cc).encode("utf-8") if self.cc else None, - b'X-Frappe-Site': get_url().encode('utf-8') + b'X-Frappe-Site': get_url().encode('utf-8'), } # reset headers as values may be changed. @@ -201,6 +201,10 @@ class EMail: del self.msg_root[key] self.msg_root[key] = val + # call hook to enable apps to modify msg_root before sending + for hook in frappe.get_hooks("make_email_body_message"): + frappe.get_attr(hook)(self) + def as_string(self): """validate, build message and convert to string""" self.validate() diff --git a/frappe/handler.py b/frappe/handler.py index c2fe2119f9..bde560e377 100755 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -90,11 +90,11 @@ def execute_cmd(cmd, async=False): if frappe.session['user'] == 'Guest': if (method not in frappe.guest_methods): frappe.msgprint(_("Not permitted")) - raise frappe.PermissionError('Not Allowed, %s' % str(method)) + raise frappe.PermissionError('Not Allowed, {0}'.format(method)) else: if not method in frappe.whitelisted: frappe.msgprint(_("Not permitted")) - raise frappe.PermissionError('Not Allowed, %s' % str(method)) + raise frappe.PermissionError('Not Allowed, {0}'.format(method)) ret = frappe.call(method, **frappe.form_dict) diff --git a/frappe/hooks.py b/frappe/hooks.py index 34eaa8a69a..14740d3852 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -26,7 +26,7 @@ to ERPNext. """ app_icon = "octicon octicon-circuit-board" -app_version = "6.0.0-wip" +app_version = "6.0.0" app_color = "orange" github_link = "https://github.com/frappe/frappe" diff --git a/frappe/website/doctype/social_login_keys/__init__.py b/frappe/integrations/__init__.py similarity index 100% rename from frappe/website/doctype/social_login_keys/__init__.py rename to frappe/integrations/__init__.py diff --git a/frappe/integrations/doctype/__init__.py b/frappe/integrations/doctype/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/social_login_keys/__init__.py b/frappe/integrations/doctype/social_login_keys/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/social_login_keys/social_login_keys.json b/frappe/integrations/doctype/social_login_keys/social_login_keys.json new file mode 100644 index 0000000000..08a028491a --- /dev/null +++ b/frappe/integrations/doctype/social_login_keys/social_login_keys.json @@ -0,0 +1,221 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "creation": "2014-03-04 08:29:52", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "System", + "fields": [ + { + "allow_on_submit": 0, + "fieldname": "facebook", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Facebook", + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "fieldname": "facebook_client_id", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Facebook Client ID", + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "fieldname": "facebook_client_secret", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Facebook Client Secret", + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "fieldname": "google", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Google", + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "fieldname": "google_client_id", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Google Client ID", + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "fieldname": "google_client_secret", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Google Client Secret", + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "fieldname": "github", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "GitHub", + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "fieldname": "github_client_id", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "GitHub Client ID", + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "fieldname": "github_client_secret", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "GitHub Client Secret", + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "icon": "icon-signin", + "idx": 1, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "modified": "2015-08-05 08:14:52.667728", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Social Login Keys", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 0, + "email": 0, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 0, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "read_only": 0, + "read_only_onload": 0 +} \ No newline at end of file diff --git a/frappe/website/doctype/social_login_keys/social_login_keys.py b/frappe/integrations/doctype/social_login_keys/social_login_keys.py similarity index 100% rename from frappe/website/doctype/social_login_keys/social_login_keys.py rename to frappe/integrations/doctype/social_login_keys/social_login_keys.py diff --git a/frappe/model/db_schema.py b/frappe/model/db_schema.py index 8c5669b526..eecb537604 100644 --- a/frappe/model/db_schema.py +++ b/frappe/model/db_schema.py @@ -140,6 +140,7 @@ class DbTable: """ fl = frappe.db.sql("SELECT * FROM tabDocField WHERE parent = %s", self.doctype, as_dict = 1) precisions = {} + uniques = {} if not frappe.flags.in_install: custom_fl = frappe.db.sql("""\ @@ -152,10 +153,15 @@ class DbTable: filters={"doc_type": self.doctype, "doctype_or_field": "DocField", "property": "precision"}): precisions[ps.field_name] = ps.value + # apply unique from property setters + for ps in frappe.get_all("Property Setter", fields=["field_name", "value"], + filters={"doc_type": self.doctype, "doctype_or_field": "DocField", "property": "unique"}): + uniques[ps.field_name] = cint(ps.value) + for f in fl: self.columns[f['fieldname']] = DbColumn(self, f['fieldname'], f['fieldtype'], f.get('length'), f.get('default'), f.get('search_index'), - f.get('options'), f.get('unique'), precisions.get(f['fieldname']) or f.get('precision')) + f.get('options'), uniques.get(f["fieldname"], f.get('unique')), precisions.get(f['fieldname']) or f.get('precision')) def get_columns_from_db(self): self.show_columns = frappe.db.sql("desc `%s`" % self.name) @@ -294,18 +300,11 @@ class DbColumn: return # type - if (current_def['type'] != column_def) or (self.unique and not current_def['unique'] \ - and column_def not in ('text', 'longtext')): + if (current_def['type'] != column_def) or \ + ((self.unique and not current_def['unique']) and column_def not in ('text', 'longtext')): self.table.change_type.append(self) else: - # index - if current_def['index'] and not self.set_index and not self.unique: - self.table.drop_index.append(self) - - if (not current_def['index'] and self.set_index) and not (column_def in ('text', 'longtext')): - self.table.add_index.append(self) - # default if (self.default_changed(current_def) \ and (self.default not in default_shortcuts) \ @@ -313,6 +312,15 @@ class DbColumn: and not (column_def in ['text','longtext'])): self.table.set_default.append(self) + # index should be applied or dropped irrespective of type change + if ( (current_def['index'] and not self.set_index and not self.unique) + or (current_def['unique'] and not self.unique) ): + # to drop unique you have to drop index + self.table.drop_index.append(self) + + elif (not current_def['index'] and self.set_index) and not (column_def in ('text', 'longtext')): + self.table.add_index.append(self) + def default_changed(self, current_def): if "decimal" in current_def['type']: return self.default_changed_for_decimal(current_def) diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 3cedda56b5..a0e795007d 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -150,27 +150,41 @@ def check_if_doc_is_linked(doc, method="Delete"): if item and item.parent != doc.name and ((method=="Delete" and item.docstatus<2) or (method=="Cancel" and item.docstatus==1)): + # raise exception only if + # linked to an non-cancelled doc when deleting + # or linked to a submitted doc when cancelling frappe.throw(_("Cannot delete or cancel because {0} {1} is linked with {2} {3}").format(doc.doctype, doc.name, item.parenttype if item.parent else link_dt, item.parent or item.name), frappe.LinkExistsError) -def check_if_doc_is_dynamically_linked(doc): +def check_if_doc_is_dynamically_linked(doc, method="Delete"): for query in dynamic_link_queries: for df in frappe.db.sql(query, as_dict=True): if frappe.get_meta(df.parent).issingle: # dynamic link in single doc refdoc = frappe.db.get_singles_dict(df.parent) - if refdoc.get(df.options)==doc.doctype and refdoc.get(df.fieldname)==doc.name: + if (refdoc.get(df.options)==doc.doctype + and refdoc.get(df.fieldname)==doc.name + and ((method=="Delete" and refdoc.docstatus < 2) + or (method=="Cancel" and refdoc.docstatus==1)) + ): + # raise exception only if + # linked to an non-cancelled doc when deleting + # or linked to a submitted doc when cancelling frappe.throw(_("Cannot delete or cancel because {0} {1} is linked with {2} {3}").format(doc.doctype, doc.name, df.parent, ""), frappe.LinkExistsError) else: - # dynamic link in table - for name in frappe.db.sql_list("""select name from `tab{parent}` where - {options}=%s and {fieldname}=%s""".format(**df), (doc.doctype, doc.name)): - frappe.throw(_("Cannot delete or cancel because {0} {1} is linked with {2} {3}").format(doc.doctype, - doc.name, df.parent, name), frappe.LinkExistsError) + for refdoc in frappe.db.sql("""select name, docstatus from `tab{parent}` where + {options}=%s and {fieldname}=%s""".format(**df), (doc.doctype, doc.name), as_dict=True): + + if ((method=="Delete" and refdoc.docstatus < 2) or (method=="Cancel" and refdoc.docstatus==1)): + # raise exception only if + # linked to an non-cancelled doc when deleting + # or linked to a submitted doc when cancelling + frappe.throw(_("Cannot delete or cancel because {0} {1} is linked with {2} {3}")\ + .format(doc.doctype, doc.name, df.parent, refdoc.name), frappe.LinkExistsError) def delete_linked_todos(doc): delete_doc("ToDo", frappe.db.sql_list("""select name from `tabToDo` diff --git a/frappe/model/document.py b/frappe/model/document.py index 7f6149dd40..823b98c228 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -457,7 +457,7 @@ class Document(BaseDocument): msgprint(msg) if frappe.flags.print_messages: - print self.as_dict() + print self.as_json().encode("utf-8") raise frappe.MandatoryError(", ".join((each[0] for each in missing))) @@ -581,7 +581,7 @@ class Document(BaseDocument): from frappe.model.delete_doc import check_if_doc_is_linked, check_if_doc_is_dynamically_linked if not self.flags.ignore_links: check_if_doc_is_linked(self, method="Cancel") - check_if_doc_is_dynamically_linked(self) + check_if_doc_is_dynamically_linked(self, method="Cancel") @staticmethod def whitelist(f): diff --git a/frappe/modules.txt b/frappe/modules.txt index e435b0231c..1b8d17eb40 100644 --- a/frappe/modules.txt +++ b/frappe/modules.txt @@ -6,3 +6,4 @@ Custom Geo Desk Print +Integrations \ No newline at end of file diff --git a/frappe/public/css/form.css b/frappe/public/css/form.css index 63db368199..6e58901425 100644 --- a/frappe/public/css/form.css +++ b/frappe/public/css/form.css @@ -161,3 +161,15 @@ select.form-control { font-weight: bold; background-color: #fffce7; } +.form-headline .alert { + font-size: 12px; + border-color: #d1d8dd; + margin-bottom: 0px; +} +.delivery-status-indicator { + display: inline-block; + margin-top: -3px; + margin-left: 1px; + font-weight: 500; + color: #8d99a6; +} diff --git a/frappe/public/js/frappe/form/control.js b/frappe/public/js/frappe/form/control.js index 1bf3f9e168..0a063be40b 100644 --- a/frappe/public/js/frappe/form/control.js +++ b/frappe/public/js/frappe/form/control.js @@ -119,6 +119,16 @@ frappe.ui.form.ControlHTML = frappe.ui.form.Control.extend({ }, html: function(html) { this.$wrapper.html(html || this.get_content()); + }, + set_value: function(html) { + if(html.appendTo) { + // jquery object + html.appendTo(this.$wrapper.empty()); + } else { + // html + this.df.options = html; + this.html(html); + } } }); diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index 240991addd..0baadef90d 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -24,7 +24,7 @@ frappe.ui.form.Dashboard = Class.extend({ }, set_headline_alert: function(text, alert_class, icon) { this.set_headline(repl('
%(icon)s%(text)s
', { - "alert_class": alert_class || "alert-info", + "alert_class": alert_class || "", "icon": icon ? ' ' : "", "text": text })); diff --git a/frappe/public/js/frappe/form/footer/timeline_item.html b/frappe/public/js/frappe/form/footer/timeline_item.html index b14d3b8856..e557b3d01c 100644 --- a/frappe/public/js/frappe/form/footer/timeline_item.html +++ b/frappe/public/js/frappe/form/footer/timeline_item.html @@ -10,15 +10,28 @@ {% if(data.doctype=="Communication" || data.comment_type=="Comment") { %} -
+
{%= data.fullname %} – {%= data.comment_on %} {% if(data.doctype=="Communication") { %} - - {%= __("Details") %} + + + {% if (data.delivery_status) { + if (in_list(["Sent", "Opened", "Clicked"], data.delivery_status)) { + var indicator_class = "green"; + } else { + var indicator_class = "red"; + } + %} + + {%= data.delivery_status %} + + {% } else { %} {%= __("Details") %} {% } %} + {%= __("Reply") %} {% } %} diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 7371d5ee7c..a4df87f059 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -53,6 +53,14 @@ frappe.ui.form.Grid = Class.extend({ var me = this, $rows = $(me.parent).find(".rows"), data = this.get_data(); + + if (this.frm && this.frm.docname) { + // use doc specific docfield object + this.df = frappe.meta.get_docfield(this.frm.doctype, this.df.fieldname, this.frm.docname); + } else { + // use non-doc specific docfield + this.df = frappe.meta.get_docfield(this.df.options, this.df.fieldname); + } this.docfields = frappe.meta.get_docfields(this.doctype, this.frm.docname); this.display_status = frappe.perm.get_field_display_status(this.df, this.frm.doc, diff --git a/frappe/public/js/frappe/ui/page.html b/frappe/public/js/frappe/ui/page.html index 188d79290c..adf6a52039 100644 --- a/frappe/public/js/frappe/ui/page.html +++ b/frappe/public/js/frappe/ui/page.html @@ -10,7 +10,7 @@
-
+
diff --git a/frappe/public/less/form.less b/frappe/public/less/form.less index 917fdb16d1..b5b6d4a980 100644 --- a/frappe/public/less/form.less +++ b/frappe/public/less/form.less @@ -209,3 +209,18 @@ select.form-control { font-weight: bold; background-color: @light-yellow; } + +.form-headline .alert { + font-size: @text-medium; + border-color: @border-color; + // background-color: @light-bg; + margin-bottom: 0px; +} + +.delivery-status-indicator { + display: inline-block; + margin-top: -3px; + margin-left: 1px; + font-weight: 500; + color: @text-muted; +} diff --git a/frappe/public/less/variables.less b/frappe/public/less/variables.less index 9f45f24f33..3cdd084e3a 100644 --- a/frappe/public/less/variables.less +++ b/frappe/public/less/variables.less @@ -10,6 +10,7 @@ @modal-backdrop-bg: #334143; @light-yellow: #fffce7; @text-extra-muted: @border-color; +@text-regular: 14px; @text-medium: 12px; @text-small: 10px; diff --git a/frappe/sessions.py b/frappe/sessions.py index b611ab3984..0763a82742 100644 --- a/frappe/sessions.py +++ b/frappe/sessions.py @@ -32,7 +32,7 @@ def clear_cache(user=None): cache = frappe.cache() groups = ("bootinfo", "user_recent", "user_roles", "user_doc", "lang", - "defaults", "user_permissions", "roles") + "defaults", "user_permissions", "roles", "home_page") if user: for name in groups: diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py index 1dbe5d0787..a3112eb481 100644 --- a/frappe/utils/boilerplate.py +++ b/frappe/utils/boilerplate.py @@ -82,7 +82,7 @@ def make_boilerplate(dest, app_name): with open(os.path.join(dest, hooks.app_name, hooks.app_name, "config", "desktop.py"), "w") as f: f.write(encode(desktop_template.format(**hooks))) - + print "'{app}' created at {path}".format(app=app_name, path=os.path.join(dest, app_name)) manifest_template = """include MANIFEST.in diff --git a/frappe/utils/redis_wrapper.py b/frappe/utils/redis_wrapper.py index 2469a7b900..7d5cd9ed0c 100644 --- a/frappe/utils/redis_wrapper.py +++ b/frappe/utils/redis_wrapper.py @@ -136,8 +136,15 @@ class RedisWrapper(redis.Redis): except redis.exceptions.ConnectionError: pass + def hdel_keys(self, name_starts_with, key): + """Delete hash names with wildcard `*` and key""" + for name in frappe.cache().get_keys(name_starts_with): + name = name.split("|", 1)[1] + self.hdel(name, key) + def hkeys(self, name): try: return super(redis.Redis, self).hkeys(self.make_key(name)) except redis.exceptions.ConnectionError: return [] + diff --git a/frappe/website/doctype/social_login_keys/social_login_keys.json b/frappe/website/doctype/social_login_keys/social_login_keys.json deleted file mode 100644 index e032acf7a2..0000000000 --- a/frappe/website/doctype/social_login_keys/social_login_keys.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "creation": "2014-03-04 08:29:52", - "docstatus": 0, - "doctype": "DocType", - "document_type": "System", - "fields": [ - { - "fieldname": "facebook", - "fieldtype": "Section Break", - "label": "Facebook", - "permlevel": 0 - }, - { - "fieldname": "facebook_client_id", - "fieldtype": "Data", - "label": "Facebook Client ID", - "permlevel": 0 - }, - { - "fieldname": "facebook_client_secret", - "fieldtype": "Data", - "label": "Facebook Client Secret", - "permlevel": 0 - }, - { - "fieldname": "google", - "fieldtype": "Section Break", - "label": "Google", - "permlevel": 0 - }, - { - "fieldname": "google_client_id", - "fieldtype": "Data", - "label": "Google Client ID", - "permlevel": 0 - }, - { - "fieldname": "google_client_secret", - "fieldtype": "Data", - "label": "Google Client Secret", - "permlevel": 0 - }, - { - "fieldname": "github", - "fieldtype": "Section Break", - "label": "GitHub", - "permlevel": 0 - }, - { - "fieldname": "github_client_id", - "fieldtype": "Data", - "label": "GitHub Client ID", - "permlevel": 0 - }, - { - "fieldname": "github_client_secret", - "fieldtype": "Data", - "label": "GitHub Client Secret", - "permlevel": 0 - } - ], - "icon": "icon-signin", - "idx": 1, - "issingle": 1, - "modified": "2015-02-05 05:11:46.875246", - "modified_by": "Administrator", - "module": "Website", - "name": "Social Login Keys", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "permlevel": 0, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ] -} \ No newline at end of file diff --git a/frappe/website/doctype/web_form/web_form.py b/frappe/website/doctype/web_form/web_form.py index a715686cbd..890b00782e 100644 --- a/frappe/website/doctype/web_form/web_form.py +++ b/frappe/website/doctype/web_form/web_form.py @@ -41,6 +41,9 @@ class WebForm(WebsiteGenerator): name = frappe.db.get_value(self.doc_type, {"owner": frappe.session.user}, "name") if name: frappe.form_dict.name = name + else: + # only a single doc allowed and no existing doc, hence new + frappe.form_dict.new = 1 # always render new form if login is not required or doesn't allow editing existing ones if not self.login_required or not self.allow_edit: diff --git a/frappe/website/render.py b/frappe/website/render.py index 9735a55544..e06f7b12b9 100644 --- a/frappe/website/render.py +++ b/frappe/website/render.py @@ -217,6 +217,7 @@ def clear_cache(path=None): clear_sitemap() frappe.clear_cache("Guest") frappe.cache().delete_value("_website_pages") + frappe.cache().delete_value("home_page") for method in frappe.get_hooks("website_clear_cache"): frappe.get_attr(method)(path) diff --git a/frappe/website/template.py b/frappe/website/template.py index 92ac2022b0..e9aa826244 100644 --- a/frappe/website/template.py +++ b/frappe/website/template.py @@ -59,8 +59,10 @@ def set_breadcrumbs(out, context): """Build breadcrumbs template (deprecated)""" out["no_breadcrumbs"] = context.get("no_breadcrumbs", 0) or ("" in out.get("content", "")) - # breadcrumbs - if not out["no_breadcrumbs"] and "breadcrumbs" not in out: + if out["no_breadcrumbs"]: + out["breadcrumbs"] = "" + + elif "breadcrumbs" not in out: out["breadcrumbs"] = frappe.get_template("templates/includes/breadcrumbs.html").render(context) def set_title_and_header(out, context): diff --git a/requirements.txt b/requirements.txt index 8520a37397..833206829e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,3 +29,4 @@ email_reply_parser click num2words gevent-socketio +watchdog==0.8.0 diff --git a/setup.py b/setup.py index 796367e206..befca47c82 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -version = "6.0.0-wip" +version = "6.0.0" with open("requirements.txt", "r") as f: install_requires = f.readlines()