From ff8215331ae42e4f89785595100ddd14656ca1fc Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Fri, 8 Sep 2017 17:17:04 +0530 Subject: [PATCH 001/135] [Add] Webhook DocType and Tests --- frappe/config/integrations.py | 6 + .../integrations/doctype/webhook/__init__.py | 0 .../doctype/webhook/test_webhook.js | 23 + .../doctype/webhook/test_webhook.py | 21 + .../integrations/doctype/webhook/webhook.js | 8 + .../integrations/doctype/webhook/webhook.json | 550 ++++++++++++++++++ .../integrations/doctype/webhook/webhook.py | 30 + .../doctype/webhook_header/__init__.py | 0 .../webhook_header/webhook_header.json | 101 ++++ .../doctype/webhook_header/webhook_header.py | 10 + .../doctype/webhook_param/__init__.py | 0 .../doctype/webhook_param/webhook_param.json | 101 ++++ .../doctype/webhook_param/webhook_param.py | 10 + 13 files changed, 860 insertions(+) create mode 100644 frappe/integrations/doctype/webhook/__init__.py create mode 100644 frappe/integrations/doctype/webhook/test_webhook.js create mode 100644 frappe/integrations/doctype/webhook/test_webhook.py create mode 100644 frappe/integrations/doctype/webhook/webhook.js create mode 100644 frappe/integrations/doctype/webhook/webhook.json create mode 100644 frappe/integrations/doctype/webhook/webhook.py create mode 100644 frappe/integrations/doctype/webhook_header/__init__.py create mode 100644 frappe/integrations/doctype/webhook_header/webhook_header.json create mode 100644 frappe/integrations/doctype/webhook_header/webhook_header.py create mode 100644 frappe/integrations/doctype/webhook_param/__init__.py create mode 100644 frappe/integrations/doctype/webhook_param/webhook_param.json create mode 100644 frappe/integrations/doctype/webhook_param/webhook_param.py diff --git a/frappe/config/integrations.py b/frappe/config/integrations.py index 92604b716b..cb69cb2a6d 100644 --- a/frappe/config/integrations.py +++ b/frappe/config/integrations.py @@ -72,6 +72,12 @@ def get_data(): "name": "GSuite Templates", "description": _("Google GSuite Templates to integration with DocTypes"), }, + { + "type": "doctype", + "name": "Webhook", + "description": _("Webhooks calling API requests into web apps"), + } + ] } ] diff --git a/frappe/integrations/doctype/webhook/__init__.py b/frappe/integrations/doctype/webhook/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/webhook/test_webhook.js b/frappe/integrations/doctype/webhook/test_webhook.js new file mode 100644 index 0000000000..799b952bed --- /dev/null +++ b/frappe/integrations/doctype/webhook/test_webhook.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Webhook", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Webhook + () => frappe.tests.make('Webhook', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/frappe/integrations/doctype/webhook/test_webhook.py b/frappe/integrations/doctype/webhook/test_webhook.py new file mode 100644 index 0000000000..7f5e071c07 --- /dev/null +++ b/frappe/integrations/doctype/webhook/test_webhook.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +class TestWebhook(unittest.TestCase): + def test_mandatory_fields(self): + base_doc = frappe.new_doc("Webhook") + + # Test for scheduler event webhook + scheduler_event_doc = base_doc + scheduler_event_doc.webhook_type = "Scheduler Event" + self.assertRaises(frappe.ValidationError, scheduler_event_doc.save) + + # Test for doc event webhook + doc_event_doc = base_doc + doc_event_doc.webhook_type = "Doc Event" + self.assertRaises(frappe.ValidationError, doc_event_doc.save) diff --git a/frappe/integrations/doctype/webhook/webhook.js b/frappe/integrations/doctype/webhook/webhook.js new file mode 100644 index 0000000000..84538f8ea1 --- /dev/null +++ b/frappe/integrations/doctype/webhook/webhook.js @@ -0,0 +1,8 @@ +// Copyright (c) 2017, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Webhook', { + refresh: function(frm) { + + } +}); diff --git a/frappe/integrations/doctype/webhook/webhook.json b/frappe/integrations/doctype/webhook/webhook.json new file mode 100644 index 0000000000..627abdcaef --- /dev/null +++ b/frappe/integrations/doctype/webhook/webhook.json @@ -0,0 +1,550 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2017-09-08 16:16:13.060641", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sb_webhook_type", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Webhook Type", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "webhook_type", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Webhook Type", + "length": 0, + "no_copy": 0, + "options": "Scheduler Event\nDoc Event", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:doc.webhook_type==\"Doc Event\"", + "fieldname": "sb_doc_events", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Doc Events", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "", + "fieldname": "webhook_doctype", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "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": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "cb_doc_events", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "webhook_docevent", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Doc Event", + "length": 0, + "no_copy": 0, + "options": "after_insert\non_update\non_submit\non_cancel\non_trash\non_update_after_submit\non_change", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:doc.webhook_type==\"Scheduler Event\"", + "fieldname": "sb_scheduler_events", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Scheduler Events ", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "scheduler_event", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Scheduler Event", + "length": 0, + "no_copy": 0, + "options": "\nall\ndaily\nhourly\nweekly\nmonthly", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sb_webhook", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Webhook Request", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "request_url", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Request URL", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "cb_webhook", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "request_verb", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Request Verb", + "length": 0, + "no_copy": 0, + "options": "GET\nPOST\nPUT", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sb_webhook_headers", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Webhook Headers", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "webhook_headers", + "fieldtype": "Table", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Headers", + "length": 0, + "no_copy": 0, + "options": "Webhook Header", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sb_webhook_params", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Webhook Params", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "webhook_params", + "fieldtype": "Table", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Params", + "length": 0, + "no_copy": 0, + "options": "Webhook Param", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-09-08 16:44:44.857115", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Webhook", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py new file mode 100644 index 0000000000..afd437b26e --- /dev/null +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.model.document import Document + +class Webhook(Document): + def autoname(self): + if self.webhook_type == "Scheduler Event" and self.scheduler_event: + self.name = self.webhook_type + "-" + self.scheduler_event + if self.webhook_type == "Doc Event" and self.webhook_doctype: + self.name = self.webhook_type + "-" + self.webhook_doctype + "-" + self.webhook_docevent + def validate(self): + self.validate_mandatory_fields() + self.validate_docevent() + def validate_mandatory_fields(self): + if self.webhook_type == "Scheduler Event": + if not self.scheduler_event: + frappe.throw(_("Select Scheduler Event")) + if self.webhook_type == "Doc Event": + if not self.webhook_doctype: + frappe.throw(_("Select DocType")) + def validate_docevent(self): + if self.doctype: + is_submittable = frappe.get_value("DocType", self.doctype, "is_submittable") + if not is_submittable and self.webhook_docevent in ["on_submit", "on_cancel", "on_update_after_submit"]: + frappe.throw(_("DocType must be Submittable for the selected Doc Event")) diff --git a/frappe/integrations/doctype/webhook_header/__init__.py b/frappe/integrations/doctype/webhook_header/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/webhook_header/webhook_header.json b/frappe/integrations/doctype/webhook_header/webhook_header.json new file mode 100644 index 0000000000..315d28335f --- /dev/null +++ b/frappe/integrations/doctype/webhook_header/webhook_header.json @@ -0,0 +1,101 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2017-09-08 16:27:39.195379", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "key", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Key", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "value", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Value", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "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-09-08 16:28:20.025612", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Webhook Header", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/webhook_header/webhook_header.py b/frappe/integrations/doctype/webhook_header/webhook_header.py new file mode 100644 index 0000000000..535b626148 --- /dev/null +++ b/frappe/integrations/doctype/webhook_header/webhook_header.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class WebhookHeader(Document): + pass diff --git a/frappe/integrations/doctype/webhook_param/__init__.py b/frappe/integrations/doctype/webhook_param/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/webhook_param/webhook_param.json b/frappe/integrations/doctype/webhook_param/webhook_param.json new file mode 100644 index 0000000000..93bd7a2dd7 --- /dev/null +++ b/frappe/integrations/doctype/webhook_param/webhook_param.json @@ -0,0 +1,101 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2017-09-08 16:28:38.852947", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "key", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Key", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "value", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Value", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "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-09-08 16:28:38.852947", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Webhook Param", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/webhook_param/webhook_param.py b/frappe/integrations/doctype/webhook_param/webhook_param.py new file mode 100644 index 0000000000..badb5eafd5 --- /dev/null +++ b/frappe/integrations/doctype/webhook_param/webhook_param.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class WebhookParam(Document): + pass From 69e0d4c930d011c474ea8edf113c81eb98b1fccf Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sat, 9 Sep 2017 12:26:47 +0530 Subject: [PATCH 002/135] Changes to Webhook DocType and Tests --- frappe/hooks.py | 17 +- .../doctype/webhook/test_webhook.py | 18 +- .../integrations/doctype/webhook/webhook.json | 248 +----------------- .../integrations/doctype/webhook/webhook.py | 17 +- 4 files changed, 25 insertions(+), 275 deletions(-) diff --git a/frappe/hooks.py b/frappe/hooks.py index f5d98f0421..2eec7997d6 100755 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -108,14 +108,25 @@ doc_events = { "*": { "on_update": [ "frappe.desk.notifications.clear_doctype_notifications", - "frappe.core.doctype.communication.feed.update_feed" + "frappe.core.doctype.communication.feed.update_feed", + "frappe.integrations.webhooks.on_update_webhook" ], "after_rename": "frappe.desk.notifications.clear_doctype_notifications", "on_cancel": [ "frappe.desk.notifications.clear_doctype_notifications", + "frappe.integrations.webhooks.on_cancel_webhook" ], - "on_trash": "frappe.desk.notifications.clear_doctype_notifications", - "on_change": "frappe.core.doctype.feedback_trigger.feedback_trigger.trigger_feedback_request" + "on_trash": [ + "frappe.desk.notifications.clear_doctype_notifications", + "frappe.integrations.webhooks.on_trash_webhook" + ], + "on_change": [ + "frappe.core.doctype.feedback_trigger.feedback_trigger.trigger_feedback_request", + "frappe.integrations.webhooks.on_change_webhook" + ], + "after_insert": "frappe.integrations.webhooks.after_insert_webhook", + "on_submit": "frappe.integrations.webhooks.on_submit_webhook", + "on_update_after_submit": "frappe.integrations.webhooks.on_update_after_submit_webhook" }, "Email Group Member": { "validate": "frappe.email.doctype.email_group.email_group.restrict_email_group" diff --git a/frappe/integrations/doctype/webhook/test_webhook.py b/frappe/integrations/doctype/webhook/test_webhook.py index 7f5e071c07..908e9b2d0e 100644 --- a/frappe/integrations/doctype/webhook/test_webhook.py +++ b/frappe/integrations/doctype/webhook/test_webhook.py @@ -7,15 +7,9 @@ import frappe import unittest class TestWebhook(unittest.TestCase): - def test_mandatory_fields(self): - base_doc = frappe.new_doc("Webhook") - - # Test for scheduler event webhook - scheduler_event_doc = base_doc - scheduler_event_doc.webhook_type = "Scheduler Event" - self.assertRaises(frappe.ValidationError, scheduler_event_doc.save) - - # Test for doc event webhook - doc_event_doc = base_doc - doc_event_doc.webhook_type = "Doc Event" - self.assertRaises(frappe.ValidationError, doc_event_doc.save) + def test_validate_docevents(self): + doc = frappe.new_doc("Webhook") + doc.webhook_doctype = "User" + doc.webhook_docevent = "on_submit" + doc.request_url = "https://httpbin.org/post" + self.assertRaises(frappe.ValidationError, doc.save) diff --git a/frappe/integrations/doctype/webhook/webhook.json b/frappe/integrations/doctype/webhook/webhook.json index 627abdcaef..ea4c868816 100644 --- a/frappe/integrations/doctype/webhook/webhook.json +++ b/frappe/integrations/doctype/webhook/webhook.json @@ -18,68 +18,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "sb_webhook_type", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Webhook Type", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "webhook_type", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Webhook Type", - "length": 0, - "no_copy": 0, - "options": "Scheduler Event\nDoc Event", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.webhook_type==\"Doc Event\"", + "depends_on": "", "fieldname": "sb_doc_events", "fieldtype": "Section Break", "hidden": 0, @@ -196,68 +135,6 @@ "set_only_once": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.webhook_type==\"Scheduler Event\"", - "fieldname": "sb_scheduler_events", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Scheduler Events ", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "scheduler_event", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Scheduler Event", - "length": 0, - "no_copy": 0, - "options": "\nall\ndaily\nhourly\nweekly\nmonthly", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -318,66 +195,6 @@ "set_only_once": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "cb_webhook", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "request_verb", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Request Verb", - "length": 0, - "no_copy": 0, - "options": "GET\nPOST\nPUT", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -438,67 +255,6 @@ "search_index": 0, "set_only_once": 0, "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sb_webhook_params", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Webhook Params", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "webhook_params", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Params", - "length": 0, - "no_copy": 0, - "options": "Webhook Param", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 } ], "has_web_view": 0, @@ -511,7 +267,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-09-08 16:44:44.857115", + "modified": "2017-09-09 12:10:09.682143", "modified_by": "Administrator", "module": "Integrations", "name": "Webhook", diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index afd437b26e..a8ba59b559 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -9,22 +9,11 @@ from frappe.model.document import Document class Webhook(Document): def autoname(self): - if self.webhook_type == "Scheduler Event" and self.scheduler_event: - self.name = self.webhook_type + "-" + self.scheduler_event - if self.webhook_type == "Doc Event" and self.webhook_doctype: - self.name = self.webhook_type + "-" + self.webhook_doctype + "-" + self.webhook_docevent + self.name = self.webhook_doctype + "-" + self.webhook_docevent def validate(self): - self.validate_mandatory_fields() self.validate_docevent() - def validate_mandatory_fields(self): - if self.webhook_type == "Scheduler Event": - if not self.scheduler_event: - frappe.throw(_("Select Scheduler Event")) - if self.webhook_type == "Doc Event": - if not self.webhook_doctype: - frappe.throw(_("Select DocType")) def validate_docevent(self): - if self.doctype: - is_submittable = frappe.get_value("DocType", self.doctype, "is_submittable") + if self.webhook_doctype: + is_submittable = frappe.get_value("DocType", self.webhook_doctype, "is_submittable") if not is_submittable and self.webhook_docevent in ["on_submit", "on_cancel", "on_update_after_submit"]: frappe.throw(_("DocType must be Submittable for the selected Doc Event")) From d6bfa7431bf454c10c7bb32f9f11501d3f535c3e Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sat, 9 Sep 2017 14:10:09 +0530 Subject: [PATCH 003/135] Webhooks for frappe doc_events --- frappe/hooks.py | 14 ++++++------ .../doctype/webhook/test_webhook.py | 6 +++++ .../integrations/doctype/webhook/webhook.json | 6 ++--- .../integrations/doctype/webhook/webhook.py | 9 ++++++++ frappe/integrations/webhooks.py | 22 +++++++++++++++++++ 5 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 frappe/integrations/webhooks.py diff --git a/frappe/hooks.py b/frappe/hooks.py index 2eec7997d6..9fdca1a87a 100755 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -109,24 +109,24 @@ doc_events = { "on_update": [ "frappe.desk.notifications.clear_doctype_notifications", "frappe.core.doctype.communication.feed.update_feed", - "frappe.integrations.webhooks.on_update_webhook" + "frappe.integrations.webhooks.doc_event_webhook" ], "after_rename": "frappe.desk.notifications.clear_doctype_notifications", "on_cancel": [ "frappe.desk.notifications.clear_doctype_notifications", - "frappe.integrations.webhooks.on_cancel_webhook" + "frappe.integrations.webhooks.doc_event_webhook" ], "on_trash": [ "frappe.desk.notifications.clear_doctype_notifications", - "frappe.integrations.webhooks.on_trash_webhook" + "frappe.integrations.webhooks.doc_event_webhook" ], "on_change": [ "frappe.core.doctype.feedback_trigger.feedback_trigger.trigger_feedback_request", - "frappe.integrations.webhooks.on_change_webhook" + "frappe.integrations.webhooks.doc_event_webhook" ], - "after_insert": "frappe.integrations.webhooks.after_insert_webhook", - "on_submit": "frappe.integrations.webhooks.on_submit_webhook", - "on_update_after_submit": "frappe.integrations.webhooks.on_update_after_submit_webhook" + "after_insert": "frappe.integrations.webhooks.doc_event_webhook", + "on_submit": "frappe.integrations.webhooks.doc_event_webhook", + "on_update_after_submit": "frappe.integrations.webhooks.doc_event_webhook" }, "Email Group Member": { "validate": "frappe.email.doctype.email_group.email_group.restrict_email_group" diff --git a/frappe/integrations/doctype/webhook/test_webhook.py b/frappe/integrations/doctype/webhook/test_webhook.py index 908e9b2d0e..c43d431670 100644 --- a/frappe/integrations/doctype/webhook/test_webhook.py +++ b/frappe/integrations/doctype/webhook/test_webhook.py @@ -13,3 +13,9 @@ class TestWebhook(unittest.TestCase): doc.webhook_docevent = "on_submit" doc.request_url = "https://httpbin.org/post" self.assertRaises(frappe.ValidationError, doc.save) + def test_validate_request_url(self): + doc = frappe.new_doc("Webhook") + doc.webhook_doctype = "User" + doc.webhook_docevent = "after_insert" + doc.request_url = "httpbin.org?post" + self.assertRaises(frappe.ValidationError, doc.save) diff --git a/frappe/integrations/doctype/webhook/webhook.json b/frappe/integrations/doctype/webhook/webhook.json index ea4c868816..4f3ba1c180 100644 --- a/frappe/integrations/doctype/webhook/webhook.json +++ b/frappe/integrations/doctype/webhook/webhook.json @@ -72,7 +72,7 @@ "report_hide": 0, "reqd": 0, "search_index": 0, - "set_only_once": 0, + "set_only_once": 1, "unique": 0 }, { @@ -132,7 +132,7 @@ "report_hide": 0, "reqd": 0, "search_index": 0, - "set_only_once": 0, + "set_only_once": 1, "unique": 0 }, { @@ -267,7 +267,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-09-09 12:10:09.682143", + "modified": "2017-09-09 13:07:14.169116", "modified_by": "Administrator", "module": "Integrations", "name": "Webhook", diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index a8ba59b559..f5c85c387b 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -6,14 +6,23 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.model.document import Document +from six.moves.urllib.parse import urlparse class Webhook(Document): def autoname(self): self.name = self.webhook_doctype + "-" + self.webhook_docevent def validate(self): self.validate_docevent() + self.validate_request_url() def validate_docevent(self): if self.webhook_doctype: is_submittable = frappe.get_value("DocType", self.webhook_doctype, "is_submittable") if not is_submittable and self.webhook_docevent in ["on_submit", "on_cancel", "on_update_after_submit"]: frappe.throw(_("DocType must be Submittable for the selected Doc Event")) + def validate_request_url(self): + try: + request_url = urlparse(self.request_url).netloc + if not request_url: + raise + except: + frappe.throw(_("Check Request URL")) diff --git a/frappe/integrations/webhooks.py b/frappe/integrations/webhooks.py new file mode 100644 index 0000000000..4c0e97664c --- /dev/null +++ b/frappe/integrations/webhooks.py @@ -0,0 +1,22 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe, requests +from frappe import _ + +# Doc Events Webhook +def doc_event_webhook(doc, method=None, *args, **kwargs): + headers = {} + webhook = frappe.get_doc("Webhook", doc.get("doctype") + "-" + method) + if webhook: + if webhook.webhook_headers: + for h in webhook.webhook_headers: + if h.get("key") and h.get("value"): + headers[h.get("key")] = h.get("value") + try: + r = requests.post(webhook.request_url, data=doc.as_json(), headers=headers, timeout=5) + frappe.logger().debug({"webhook_success":r.text, "webhook": webhook.as_json()}) + except: + frappe.logger().debug({"webhook_error":r.text, "webhook": webhook.as_json()}) + frappe.throw(_("Unable to make request")) From 0b1a655da1210aac4bd34676ad7bd160499eef6e Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sat, 9 Sep 2017 19:26:50 +0530 Subject: [PATCH 004/135] [Fix] filter only existing webhooks --- frappe/integrations/webhooks.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frappe/integrations/webhooks.py b/frappe/integrations/webhooks.py index 4c0e97664c..1312d63712 100644 --- a/frappe/integrations/webhooks.py +++ b/frappe/integrations/webhooks.py @@ -8,7 +8,12 @@ from frappe import _ # Doc Events Webhook def doc_event_webhook(doc, method=None, *args, **kwargs): headers = {} - webhook = frappe.get_doc("Webhook", doc.get("doctype") + "-" + method) + filters = { + "webhook_doctype": doc.get("doctype"), + "webhook_docevent": method + } + webhooks = frappe.get_all("Webhook", filters=filters) + webhook = frappe.get_doc("Webhook", webhooks[0].get("name")) if webhooks and len(webhooks) > 0 else None if webhook: if webhook.webhook_headers: for h in webhook.webhook_headers: From f8a1d55689f317fccb612e6dfdea85ee220ac603 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sun, 10 Sep 2017 10:58:33 +0530 Subject: [PATCH 005/135] Fix and Documentation webhook_doctype field made Mandatory in Webhook Documentation and screenshot added --- frappe/docs/assets/img/webhook.png | Bin 0 -> 34615 bytes .../user/en/guides/integration/webhooks.md | 18 ++++++++++++++++++ .../integrations/doctype/webhook/webhook.json | 4 ++-- 3 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 frappe/docs/assets/img/webhook.png create mode 100644 frappe/docs/user/en/guides/integration/webhooks.md diff --git a/frappe/docs/assets/img/webhook.png b/frappe/docs/assets/img/webhook.png new file mode 100644 index 0000000000000000000000000000000000000000..cbc16c1d76469689b62c77cee8db22175f5b0ca1 GIT binary patch literal 34615 zcmdqJWmH_v_BV(G37+5%A-KD{1&81s+}#~Qf(CbYcXyY@t#S9p-K80F-}~JEedp86 ze3&(B&gymQRMjrqyLO$utA3|L73C$75%3ToARv&XzKMN@fPhhifPl(@hkZ|J#QyF1 z9(-^Tky3$wzr5j%L*C=K&f*%*%66vCZU&Af5N5V^HYW5=Mvf*Xwoc}D&galw0uT@+ z5K>~oD(>kgYhK>*w;A_u>ih9y)T}84!=b-^eTadFO@I#>=*_*YR<*ENtE_iB25YUg zSrWH3v}t>}mHC`3w_2?{&!6e8c&Q(I3e=MzV1_%gU_?y)U?LS7GH^^6WdT{CJiao= zp~a?r_?nC+I0=A8cG6wAf6`m{)l%u9zag5+n2kjC3rz%`^xro_s5x16q5nkRGfZhe zD*pa2iZ3w#=lJ+PCA@i%Jll~x`KY#rWYxvtN#*l^metAQ8e@eim0@TiA(~Wj6fvLw z)fi|c8xWpLwF@$isRT|qyZl1iW+Nq+J@-e{ZAA%}5>a{YP*EIK@xO?-+wpJQsc31U zFI3=}X$y|)--}Jp&#}@%&3^kj53(45i$prSG}2tN`o`YmdILA3miGHgF4Hu}9#h0K5V?tP5?bX&KNLu|N( z9jmf&kX0Lkr+Wy@3(Y|j8-YQP3!5Ia+UfeAvl0Gjv`)->D9*{wgGaAbo|86+3eB+b z`Z*9(aiXZmZD$gGEd%wJfB)j$`)#7`5bv{)_tjS+Oje4M>+PrsXL>Oj=7C8?MS<0S zM4SJaJbwt=i}_y4pT;z(Dk&<`ZN|AgiS)5N!^co>S!>0@)~0CXo*GptPnZw#(Dc3j zEQA^=>0M>twKm0pFTa%6l?UM!fX{g0zb>+>vPtlSZNrO1(Skc7@&GFb&KX^{&4zTG z#OA6QL}`eDEGZX^~dukB4c}a z<^>DWR;X>G?~yf+EvVIIYIT>_?7I+DHr768{5&kQOAn7GmkXw}9SPn$Xp|icT9Lx4 z;sPm0DV4KGID8}f3uzbXmBu@JEp;H`{1G8NHP|ySNd_fPPJ6P)V2bbh_GaM1{e@ok zuQb~Va=+E@Uazi=4W-RD*T*fhY;tV`*1H26oIBKQvApvb>|`exXAG9MA?(Cw)5Iw2!(OH%YRBYbBZ4f_pgD7?I0pgWuk z_iCy9u3{(jK1eoY2=DM=8Cw*!oG65o*myNwy+ooOhUFVgOrc#oP3@~y9|d*o79Jh8 z3a88EHn;K|D&1ft?#Jvdyb(M;Tp!z2+X;!Uo8UK_)G(Xy^94*T2||QCIiLn9&7QeW z_Dda|yLrI(zcj-I3WZ%7z(v)%fF#`!j+!eN?yap@eN1!|gC2g25-*{U297kW?!q@&#b(z5zO}~!fTx-M0NB;$UW0gHVkJ{0|1bn#`}b1wqJj-S zdD7CZ!zjMnOmy7=OE94HXMdT}mtu4P{$BsYMqP)f+<=)amy0b$$kts@D1|P;0P#gm z|CpI>my~!+B8dn*Gg+>C#_OW4u!xdkNkek2Q;J;%SV~nus-}l166r`&!&2(@R^MBv zuLO(g2K9qf6DCD3kSXw0{wWCRLtbBgNdpdR9rd_e-n|&{bi1MS#I@*-`49N#ZW7TR zcxDxI=R;%AevA?8)W)gdRBW&~%U#`W!QsHh#jz=E)?=^k%Vf9{6U47Iw$qIX#IDz_ zoAno$GacKE!!Y@(DYOYiywZ9Id4&(*E~#B|H^c=F5F`@%qe`$?3QlB%x*v}P?79OI zUZ^|a_Gk6R2V+V0SI1^)sfkuaNx#69sHe<0$CC?i5h*p2lE$T8iK}ar+VE98_#(3CkElS_S9EG-lCc1JkhT z*a!8c(0WGjkw%VGDnv_ZjPG@6Z5rRPBH3XZ_hvH(!l^!(Q-86KIqY#2`gfD!7lD?%0N
`|Y>FGiHC3u8>X)CS6tHA23Bm>secq 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 v_8R Integrations > External Documents > Webhook + +Webhook + + + +1. Select the DocType for which hook needs to be triggered e.g. Note +2. Select the DocEvent for which hook needs to be triggered e.g. on_trash +3. Enter a valid request URL. On occurence of DocEvent, POST request with doc's json as data is made to the URL. +4. Optionally you can add headers to the request to be made. Useful for sending api key if required. diff --git a/frappe/integrations/doctype/webhook/webhook.json b/frappe/integrations/doctype/webhook/webhook.json index 4f3ba1c180..1823987534 100644 --- a/frappe/integrations/doctype/webhook/webhook.json +++ b/frappe/integrations/doctype/webhook/webhook.json @@ -70,7 +70,7 @@ "read_only": 0, "remember_last_selected_value": 0, "report_hide": 0, - "reqd": 0, + "reqd": 1, "search_index": 0, "set_only_once": 1, "unique": 0 @@ -267,7 +267,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-09-09 13:07:14.169116", + "modified": "2017-09-09 20:05:43.406391", "modified_by": "Administrator", "module": "Integrations", "name": "Webhook", From 3d15e6a43151c1a0c7ae22265dd577871a5bed8f Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sun, 10 Sep 2017 11:09:50 +0530 Subject: [PATCH 006/135] Cleanup --- .../doctype/webhook_param/__init__.py | 0 .../doctype/webhook_param/webhook_param.json | 101 ------------------ .../doctype/webhook_param/webhook_param.py | 10 -- 3 files changed, 111 deletions(-) delete mode 100644 frappe/integrations/doctype/webhook_param/__init__.py delete mode 100644 frappe/integrations/doctype/webhook_param/webhook_param.json delete mode 100644 frappe/integrations/doctype/webhook_param/webhook_param.py diff --git a/frappe/integrations/doctype/webhook_param/__init__.py b/frappe/integrations/doctype/webhook_param/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/integrations/doctype/webhook_param/webhook_param.json b/frappe/integrations/doctype/webhook_param/webhook_param.json deleted file mode 100644 index 93bd7a2dd7..0000000000 --- a/frappe/integrations/doctype/webhook_param/webhook_param.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-09-08 16:28:38.852947", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "key", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Key", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "value", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Value", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "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-09-08 16:28:38.852947", - "modified_by": "Administrator", - "module": "Integrations", - "name": "Webhook Param", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/frappe/integrations/doctype/webhook_param/webhook_param.py b/frappe/integrations/doctype/webhook_param/webhook_param.py deleted file mode 100644 index badb5eafd5..0000000000 --- a/frappe/integrations/doctype/webhook_param/webhook_param.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2017, Frappe Technologies and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe.model.document import Document - -class WebhookParam(Document): - pass From 8c71042c6765860c29e446394c34fd7dcc6f7f82 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sun, 10 Sep 2017 11:49:53 +0530 Subject: [PATCH 007/135] [fix] Codacy errors --- frappe/integrations/doctype/webhook/webhook.py | 2 +- frappe/integrations/webhooks.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index f5c85c387b..de62c9e833 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -24,5 +24,5 @@ class Webhook(Document): request_url = urlparse(self.request_url).netloc if not request_url: raise - except: + except Exception as e: frappe.throw(_("Check Request URL")) diff --git a/frappe/integrations/webhooks.py b/frappe/integrations/webhooks.py index 1312d63712..af4cba11cf 100644 --- a/frappe/integrations/webhooks.py +++ b/frappe/integrations/webhooks.py @@ -22,6 +22,6 @@ def doc_event_webhook(doc, method=None, *args, **kwargs): try: r = requests.post(webhook.request_url, data=doc.as_json(), headers=headers, timeout=5) frappe.logger().debug({"webhook_success":r.text, "webhook": webhook.as_json()}) - except: + except Exception as e: frappe.logger().debug({"webhook_error":r.text, "webhook": webhook.as_json()}) frappe.throw(_("Unable to make request")) From a928d7ac01fdea804fbabf91e07d9b52b43250e4 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sun, 10 Sep 2017 12:03:50 +0530 Subject: [PATCH 008/135] [Fix] Codacy --- frappe/integrations/doctype/webhook/webhook.py | 4 ++-- frappe/integrations/webhooks.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index de62c9e833..ff86c67c7f 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -23,6 +23,6 @@ class Webhook(Document): try: request_url = urlparse(self.request_url).netloc if not request_url: - raise + raise frappe.ValidationError except Exception as e: - frappe.throw(_("Check Request URL")) + frappe.throw(_("Check Request URL"), exc=e) diff --git a/frappe/integrations/webhooks.py b/frappe/integrations/webhooks.py index af4cba11cf..219dceaa8e 100644 --- a/frappe/integrations/webhooks.py +++ b/frappe/integrations/webhooks.py @@ -24,4 +24,4 @@ def doc_event_webhook(doc, method=None, *args, **kwargs): frappe.logger().debug({"webhook_success":r.text, "webhook": webhook.as_json()}) except Exception as e: frappe.logger().debug({"webhook_error":r.text, "webhook": webhook.as_json()}) - frappe.throw(_("Unable to make request")) + frappe.throw(_("Unable to make request"), exc=e) From 4b75ec13b32ddec9995815ba3effdedcc9d7ffb3 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sun, 10 Sep 2017 18:30:50 +0530 Subject: [PATCH 009/135] [Fix] Codacy Errors --- frappe/integrations/doctype/webhook/webhook.js | 2 +- frappe/integrations/doctype/webhook_header/webhook_header.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/webhook/webhook.js b/frappe/integrations/doctype/webhook/webhook.js index 84538f8ea1..91ec3b3e99 100644 --- a/frappe/integrations/doctype/webhook/webhook.js +++ b/frappe/integrations/doctype/webhook/webhook.js @@ -2,7 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on('Webhook', { - refresh: function(frm) { + refresh: function(/*frm*/) { } }); diff --git a/frappe/integrations/doctype/webhook_header/webhook_header.py b/frappe/integrations/doctype/webhook_header/webhook_header.py index 535b626148..11d3ee4085 100644 --- a/frappe/integrations/doctype/webhook_header/webhook_header.py +++ b/frappe/integrations/doctype/webhook_header/webhook_header.py @@ -3,7 +3,7 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe +# import frappe from frappe.model.document import Document class WebhookHeader(Document): From beb6318d88b8081aca73e4d9103e426cb58dc7a6 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Thu, 14 Sep 2017 13:24:02 +0530 Subject: [PATCH 010/135] Webhook Data DocType added --- .../integrations/doctype/webhook/webhook.js | 40 +++++- .../integrations/doctype/webhook/webhook.json | 63 ++++++++- .../integrations/doctype/webhook/webhook.py | 9 ++ .../doctype/webhook_data/__init__.py | 0 .../doctype/webhook_data/webhook_data.json | 130 ++++++++++++++++++ .../doctype/webhook_data/webhook_data.py | 10 ++ 6 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 frappe/integrations/doctype/webhook_data/__init__.py create mode 100644 frappe/integrations/doctype/webhook_data/webhook_data.json create mode 100644 frappe/integrations/doctype/webhook_data/webhook_data.py diff --git a/frappe/integrations/doctype/webhook/webhook.js b/frappe/integrations/doctype/webhook/webhook.js index 91ec3b3e99..b31d7e9661 100644 --- a/frappe/integrations/doctype/webhook/webhook.js +++ b/frappe/integrations/doctype/webhook/webhook.js @@ -1,8 +1,46 @@ // Copyright (c) 2017, Frappe Technologies and contributors // For license information, please see license.txt +frappe.webhook = { + set_fieldname_select: function(frm) { + var me = this, + doc = frm.doc; + if (doc.webhook_doctype) { + frappe.model.with_doctype(doc.webhook_doctype, function() { + var fields = $.map(frappe.get_doc("DocType", frm.doc.webhook_doctype).fields, function(d) { + if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 || + d.fieldtype === 'Table') { + return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname } + } + else if (d.fieldtype === 'Currency' || d.fieldtype === 'Float') { + return { label: d.label, value: d.fieldname } + } + else { + return null; + } + }) + frappe.meta.get_docfield("Webhook Data", "fieldname", frm.doc.name).options = [""].concat(fields); + }); + } + } +} + frappe.ui.form.on('Webhook', { - refresh: function(/*frm*/) { + refresh: function(frm) { + frappe.webhook.set_fieldname_select(frm); + }, + webhook_doctype: function(frm) { + frappe.webhook.set_fieldname_select(frm); + } +}); +frappe.ui.form.on("Webhook Data", { + fieldname: function(frm, doctype, name) { + var doc = frappe.get_doc(doctype, name); + var df = $.map(frappe.get_doc("DocType", frm.doc.webhook_doctype).fields, function(d) { + return doc.fieldname == d.fieldname ? d : null; + })[0]; + doc.key = df.fieldname + frm.refresh_field("webhook_data"); } }); diff --git a/frappe/integrations/doctype/webhook/webhook.json b/frappe/integrations/doctype/webhook/webhook.json index 1823987534..1a3866085a 100644 --- a/frappe/integrations/doctype/webhook/webhook.json +++ b/frappe/integrations/doctype/webhook/webhook.json @@ -255,6 +255,67 @@ "search_index": 0, "set_only_once": 0, "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sb_webhook_data", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Webhook Data", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "webhook_data", + "fieldtype": "Table", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Data", + "length": 0, + "no_copy": 0, + "options": "Webhook Data", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], "has_web_view": 0, @@ -267,7 +328,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-09-09 20:05:43.406391", + "modified": "2017-09-14 13:16:53.974340", "modified_by": "Administrator", "module": "Integrations", "name": "Webhook", diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index ff86c67c7f..b65fbfa486 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -14,6 +14,7 @@ class Webhook(Document): def validate(self): self.validate_docevent() self.validate_request_url() + self.validate_repeating_companies() def validate_docevent(self): if self.webhook_doctype: is_submittable = frappe.get_value("DocType", self.webhook_doctype, "is_submittable") @@ -26,3 +27,11 @@ class Webhook(Document): raise frappe.ValidationError except Exception as e: frappe.throw(_("Check Request URL"), exc=e) + def validate_repeating_companies(self): + """Error when Same Field is entered multiple times in webhook_data""" + webhook_data = [] + for entry in self.webhook_data: + webhook_data.append(entry.fieldname) + + if len(webhook_data)!= len(set(webhook_data)): + frappe.throw(_("Same Field is entered more than once")) diff --git a/frappe/integrations/doctype/webhook_data/__init__.py b/frappe/integrations/doctype/webhook_data/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/webhook_data/webhook_data.json b/frappe/integrations/doctype/webhook_data/webhook_data.json new file mode 100644 index 0000000000..96ae7f786a --- /dev/null +++ b/frappe/integrations/doctype/webhook_data/webhook_data.json @@ -0,0 +1,130 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2017-09-14 12:08:50.302810", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "fieldname", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Fieldname", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "cb_doc_data", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "key", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Key", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "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-09-14 13:16:58.252176", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Webhook Data", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 0, + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/webhook_data/webhook_data.py b/frappe/integrations/doctype/webhook_data/webhook_data.py new file mode 100644 index 0000000000..f968a6424e --- /dev/null +++ b/frappe/integrations/doctype/webhook_data/webhook_data.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class WebhookData(Document): + pass From aca5c147c9e3c99bd12c14320087a08da34ca789 Mon Sep 17 00:00:00 2001 From: mbauskar Date: Thu, 14 Sep 2017 13:46:19 +0530 Subject: [PATCH 011/135] [minor] assingnment should not be allowed in the depends_on and collapsible_depends_on properties --- frappe/core/doctype/doctype/doctype.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index b0fff5b941..cf5e5c3fe0 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -595,6 +595,15 @@ def validate_fields(meta): frappe.throw(_("Sort field {0} must be a valid fieldname").format(fieldname), InvalidFieldNameError) + def check_illegal_depends_on_conditions(docfield): + ''' assignment operation should not be allowed in the depends on condition.''' + depends_on_fields = ["depends_on", "collapsible_depends_on"] + for field in depends_on_fields: + depends_on = docfield.get(field, None) + if depends_on and ("=" in depends_on) and \ + re.match("""[\w\.:_]+\s*={1}\s*[\w\.@'"]+""", depends_on): + frappe.throw(_("Invalid {0} condition").format(frappe.unscrub(field)), frappe.ValidationError) + fields = meta.get("fields") fieldname_list = [d.fieldname for d in fields] @@ -620,6 +629,7 @@ def validate_fields(meta): check_in_global_search(d) check_illegal_default(d) check_unique_and_text(d) + check_illegal_depends_on_conditions(d) check_fold(fields) check_search_fields(meta, fields) From 618ceb8982cdb83e7242182ff18ad9fd258b1607 Mon Sep 17 00:00:00 2001 From: mbauskar Date: Thu, 14 Sep 2017 13:46:30 +0530 Subject: [PATCH 012/135] [test-cases] minor fixes and checked all the depends on fields are valid of not --- frappe/core/doctype/doctype/test_doctype.py | 41 +++++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index 81320c2f38..41a4267c97 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -10,13 +10,22 @@ import unittest class TestDocType(unittest.TestCase): - def new_doctype(self, name, unique=0): + def new_doctype(self, name, unique=0, depends_on=''): return frappe.get_doc({ "doctype": "DocType", "module": "Core", "custom": 1, - "fields": [{"label": "Some Field", "fieldname": "some_fieldname", "fieldtype": "Data", "unique": unique}], - "permissions": [{"role": "System Manager", "read": 1}], + "fields": [{ + "label": "Some Field", + "fieldname": "some_fieldname", + "fieldtype": "Data", + "unique": unique, + "depends_on": depends_on, + }], + "permissions": [{ + "role": "System Manager", + "read": 1 + }], "name": name }) @@ -71,4 +80,28 @@ class TestDocType(unittest.TestCase): field.fieldtype = "HTML" field.label = "Some HTML Field" doc.search_fields = "some_fieldname,some_html_field" - self.assertRaises(frappe.ValidationError, doc.save) \ No newline at end of file + self.assertRaises(frappe.ValidationError, doc.save) + + def test_depends_on_fields(self): + doc = self.new_doctype("Test Depends On", depends_on="eval:doc.__islocal == 0") + doc.insert() + + # check if the assignment operation is allowed in depends_on + field = doc.fields[0] + field.depends_on = "eval:doc.__islocal = 0" + self.assertRaises(frappe.ValidationError, doc.save) + + def test_all_depends_on_fields_conditions(self): + import re + + docfields = frappe.get_all("DocField", or_filters={ + "ifnull(depends_on, '')": ("!=", ''), + "ifnull(collapsible_depends_on, '')": ("!=", '') + }, fields=["parent", "depends_on", "collapsible_depends_on", "fieldname", "fieldtype"]) + + pattern = """[\w\.:_]+\s*={1}\s*[\w\.@'"]+""" + for field in docfields: + for depends_on in ["depends_on", "collapsible_depends_on"]: + condition = field.get(depends_on) + if condition: + self.assertFalse(re.match(pattern, condition)) \ No newline at end of file From 24a2656746cd997e2bc2fa5854523993130ca460 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Thu, 14 Sep 2017 14:16:44 +0530 Subject: [PATCH 013/135] Only allow specified fields of doc --- frappe/integrations/doctype/webhook/webhook.py | 4 ++-- frappe/integrations/webhooks.py | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index b65fbfa486..5f74b22ac4 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -14,7 +14,7 @@ class Webhook(Document): def validate(self): self.validate_docevent() self.validate_request_url() - self.validate_repeating_companies() + self.validate_repeating_fields() def validate_docevent(self): if self.webhook_doctype: is_submittable = frappe.get_value("DocType", self.webhook_doctype, "is_submittable") @@ -27,7 +27,7 @@ class Webhook(Document): raise frappe.ValidationError except Exception as e: frappe.throw(_("Check Request URL"), exc=e) - def validate_repeating_companies(self): + def validate_repeating_fields(self): """Error when Same Field is entered multiple times in webhook_data""" webhook_data = [] for entry in self.webhook_data: diff --git a/frappe/integrations/webhooks.py b/frappe/integrations/webhooks.py index 219dceaa8e..466a0f0470 100644 --- a/frappe/integrations/webhooks.py +++ b/frappe/integrations/webhooks.py @@ -2,12 +2,13 @@ # MIT License. See license.txt from __future__ import unicode_literals -import frappe, requests +import frappe, requests, json from frappe import _ # Doc Events Webhook def doc_event_webhook(doc, method=None, *args, **kwargs): headers = {} + data = {} filters = { "webhook_doctype": doc.get("doctype"), "webhook_docevent": method @@ -19,8 +20,13 @@ def doc_event_webhook(doc, method=None, *args, **kwargs): for h in webhook.webhook_headers: if h.get("key") and h.get("value"): headers[h.get("key")] = h.get("value") + if webhook.webhook_data: + for k, v in doc.as_dict().items(): + for w in webhook.webhook_data: + if k == w.fieldname: + data[w.key] = v try: - r = requests.post(webhook.request_url, data=doc.as_json(), headers=headers, timeout=5) + r = requests.post(webhook.request_url, data=json.dumps(data), headers=headers, timeout=5) frappe.logger().debug({"webhook_success":r.text, "webhook": webhook.as_json()}) except Exception as e: frappe.logger().debug({"webhook_error":r.text, "webhook": webhook.as_json()}) From cedb309446651867a6bbea739b1db0c2ca7649fb Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Thu, 14 Sep 2017 17:26:34 +0530 Subject: [PATCH 014/135] Cleanup and Docs for Webhooks --- .../user/en/guides/integration/webhooks.md | 87 ++++++++++++++++++- .../integrations/doctype/webhook/webhook.js | 14 +-- .../doctype/webhook_data/webhook_data.py | 2 +- 3 files changed, 94 insertions(+), 9 deletions(-) diff --git a/frappe/docs/user/en/guides/integration/webhooks.md b/frappe/docs/user/en/guides/integration/webhooks.md index bad812ed6e..30d90fdd37 100644 --- a/frappe/docs/user/en/guides/integration/webhooks.md +++ b/frappe/docs/user/en/guides/integration/webhooks.md @@ -1,6 +1,6 @@ # Webhooks -Webhooks are "user-defined HTTP callbacks". You can create webhook which triggers on Doc Event of the selected DocType. When the `doc_events` occurs, the source site makes an HTTP request to the URI configured for the webhook. Users can configure them to cause events on one site to invoke behaviour on another. +Webhooks are "user-defined HTTP callbacks". You can create webhook which triggers on Doc Event of the selected DocType. When the `doc_events` occurs, the source site makes an HTTP request to the URI configured for the webhook. Users can configure them to cause events on one site to invoke behaviour on another. #### Configure Webhook @@ -16,3 +16,88 @@ Webhook 2. Select the DocEvent for which hook needs to be triggered e.g. on_trash 3. Enter a valid request URL. On occurence of DocEvent, POST request with doc's json as data is made to the URL. 4. Optionally you can add headers to the request to be made. Useful for sending api key if required. +5. Optionally you can select fields and set its `key` to be sent as data json + +e.g. Webhook + +- **DocType** : `Quotation` +- **Doc Event** : `on_update` +- **Request URL** : `https://httpbin.org/post` +- **Webhook Data** : + 1. **Fieldname** : `name` and **Key** : `id` + 2. **Fieldname** : `items` and **Key** : `lineItems` + +Note: if no headers or data is present, request will be made without any header or body + +Example request sent by frappe server on `Quotation` - `on_update` to https://httpbin.org/post: + +``` +{ + "args": {}, + "data": "{\"lineItems\": [{\"stock_qty\": 1.0, \"base_price_list_rate\": 1.0, \"image\": \"\", \"creation\": \"2017-09-14 13:41:58.373023\", \"base_amount\": 1.0, \"qty\": 1.0, \"margin_rate_or_amount\": 0.0, \"rate\": 1.0, \"owner\": \"Administrator\", \"stock_uom\": \"Unit\", \"base_net_amount\": 1.0, \"page_break\": 0, \"modified_by\": \"Administrator\", \"base_net_rate\": 1.0, \"discount_percentage\": 0.0, \"item_name\": \"I1\", \"amount\": 1.0, \"actual_qty\": 0.0, \"net_rate\": 1.0, \"conversion_factor\": 1.0, \"warehouse\": \"Finished Goods - R\", \"docstatus\": 0, \"prevdoc_docname\": null, \"uom\": \"Unit\", \"description\": \"I1\", \"parent\": \"QTN-00001\", \"brand\": null, \"gst_hsn_code\": null, \"base_rate\": 1.0, \"item_code\": \"I1\", \"projected_qty\": 0.0, \"margin_type\": \"\", \"doctype\": \"Quotation Item\", \"rate_with_margin\": 0.0, \"pricing_rule\": null, \"price_list_rate\": 1.0, \"name\": \"QUOD/00001\", \"idx\": 1, \"item_tax_rate\": \"{}\", \"item_group\": \"Products\", \"modified\": \"2017-09-14 17:09:51.239271\", \"parenttype\": \"Quotation\", \"customer_item_code\": null, \"net_amount\": 1.0, \"prevdoc_doctype\": null, \"parentfield\": \"items\"}], \"id\": \"QTN-00001\"}", + "files": {}, + "form": {}, + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Connection": "close", + "Content-Length": "1075", + "Host": "httpbin.org", + "User-Agent": "python-requests/2.18.1" + }, + "json": { + "id": "QTN-00001", + "lineItems": [ + { + "actual_qty": 0.0, + "amount": 1.0, + "base_amount": 1.0, + "base_net_amount": 1.0, + "base_net_rate": 1.0, + "base_price_list_rate": 1.0, + "base_rate": 1.0, + "brand": null, + "conversion_factor": 1.0, + "creation": "2017-09-14 13:41:58.373023", + "customer_item_code": null, + "description": "I1", + "discount_percentage": 0.0, + "docstatus": 0, + "doctype": "Quotation Item", + "gst_hsn_code": null, + "idx": 1, + "image": "", + "item_code": "I1", + "item_group": "Products", + "item_name": "I1", + "item_tax_rate": "{}", + "margin_rate_or_amount": 0.0, + "margin_type": "", + "modified": "2017-09-14 17:09:51.239271", + "modified_by": "Administrator", + "name": "QUOD/00001", + "net_amount": 1.0, + "net_rate": 1.0, + "owner": "Administrator", + "page_break": 0, + "parent": "QTN-00001", + "parentfield": "items", + "parenttype": "Quotation", + "prevdoc_docname": null, + "prevdoc_doctype": null, + "price_list_rate": 1.0, + "pricing_rule": null, + "projected_qty": 0.0, + "qty": 1.0, + "rate": 1.0, + "rate_with_margin": 0.0, + "stock_qty": 1.0, + "stock_uom": "Unit", + "uom": "Unit", + "warehouse": "Finished Goods - R" + } + ] + }, + "url": "https://httpbin.org/post" +} +``` \ No newline at end of file diff --git a/frappe/integrations/doctype/webhook/webhook.js b/frappe/integrations/doctype/webhook/webhook.js index b31d7e9661..bff8591592 100644 --- a/frappe/integrations/doctype/webhook/webhook.js +++ b/frappe/integrations/doctype/webhook/webhook.js @@ -3,27 +3,27 @@ frappe.webhook = { set_fieldname_select: function(frm) { - var me = this, - doc = frm.doc; + doc = frm.doc; if (doc.webhook_doctype) { frappe.model.with_doctype(doc.webhook_doctype, function() { var fields = $.map(frappe.get_doc("DocType", frm.doc.webhook_doctype).fields, function(d) { if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 || d.fieldtype === 'Table') { - return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname } + return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname }; } else if (d.fieldtype === 'Currency' || d.fieldtype === 'Float') { - return { label: d.label, value: d.fieldname } + return { label: d.label, value: d.fieldname }; } else { return null; } - }) + }); + fields.unshift({"label":"Name (Doc Name)","value":"name"}) frappe.meta.get_docfield("Webhook Data", "fieldname", frm.doc.name).options = [""].concat(fields); }); } } -} +}; frappe.ui.form.on('Webhook', { refresh: function(frm) { @@ -40,7 +40,7 @@ frappe.ui.form.on("Webhook Data", { var df = $.map(frappe.get_doc("DocType", frm.doc.webhook_doctype).fields, function(d) { return doc.fieldname == d.fieldname ? d : null; })[0]; - doc.key = df.fieldname + doc.key = df != undefined ? df.fieldname : "name"; frm.refresh_field("webhook_data"); } }); diff --git a/frappe/integrations/doctype/webhook_data/webhook_data.py b/frappe/integrations/doctype/webhook_data/webhook_data.py index f968a6424e..b7d989410f 100644 --- a/frappe/integrations/doctype/webhook_data/webhook_data.py +++ b/frappe/integrations/doctype/webhook_data/webhook_data.py @@ -3,7 +3,7 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe +# import frappe from frappe.model.document import Document class WebhookData(Document): From 369ce0af6695a332f89a8eb5cbbf5612da6941cd Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Thu, 14 Sep 2017 17:30:08 +0530 Subject: [PATCH 015/135] [Docs] Webhook screenshot updated --- frappe/docs/assets/img/webhook.png | Bin 34615 -> 52412 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/frappe/docs/assets/img/webhook.png b/frappe/docs/assets/img/webhook.png index cbc16c1d76469689b62c77cee8db22175f5b0ca1..859b5f57bdbc5c2a99fb4494b19e21087ed693ec 100644 GIT binary patch literal 52412 zcmdSAWo%?Uvo4xRCd|yqWWt3!o2Zp7D1(K5BW7e+x!>t9ew-4;8ydx}#F;0pdyg9sqy~IP zit7P9yFA5=Yut8n+fNWJFh<)U&Mk4S-5Cr`D6A@#S2mL<_B&m@op#JMI1c9&+ zztI1<9VihkA^T6`uWm3y7(ix>#o~T%Em?weA zA3{zmrsR$GH);LCjZ=Z&zt2*Si6~z}X+$6hq~{8QY?|8S;!x$Ok*J^u23ljX!W0JF zFHN4`Qoy2(EmkEY`~wRSyBdD7GsFJPAJ7-6!DlukGAII)LIa^_E{4pLpdvA3ilBZ( z_BSsNuTQqeE2TSjDkBDRBL?{#J7j789mb-@+7&N-{FE-Uv^`k1iDf38#oVkck=NxYZLXq z!qYRs_2NN`kEK#yWdjcb8V%!5qJ|A06%C$Z0Yind)t&QtfgJt3w4U+svXBH80-68} z({WypsidQ`iX{~JOio^#yH^A$K>w>74BYH3nXo~|!}B5)$)_Q*ABemBMW5TWdy7ps zhuC|I4+aFedH>Nf{Wm=Y;S6{9LEku3!&#VXG`(MQieB7amP-p!>TB_VnoH*S6~Xp+=VXZf(Q zZys_W3C~lIVog)5Es6`eS9M?~%5+_zynNF5gVb0oNzh8QCuG zKNrrXj7YO=n;sW@C~`^CrhM!@tmrC7dG&F}HN3plN(>t7{*^>phUWBc zN-EOG&_dYD9r$I80Y7mSvu|R5aYc{Ya@dN53Nx9Iyv)1%hQ6q?UCh&23X}LJH_4h> zne=5RP2DkQBnaorG4^+|>hj#*d&wK`$I%ZhHP7UPD@1PWqZ#WaJ`Srwwo)$NoH#vX z-Q_l8Ob2pY2f<3VpSg8?BnF~r`0VI0E)R(XM;A0`7#T%)&f{8cBvdcIu{g8vyeS8b-EbkY~V{H*W9Gt!#ip&7$GoIzA%veP=rXt+u0f9!@m z^ZR@U4j^gE8P%QL$Jc|JC5qS7_`c8xz@sW7UB0<7W|bFLxlIJ^_aYtM#=CsP#kCvFa5o44yAwvq1D+dc~J zcnXC+Xo!~zpj&;qjkGZv&UWpV9vi3KSah}k3*EgA3?b^^;Qw(X5~(Jlxs|H1U(7=g zkTRcFmK;O^kE^MhXEKdVwGS}V$(+?zblPQIkR)pN>4W^_zuZN0Ni_=Xbp6>2 zhf{g(;4rkMK1y=Nrdj}&&bnx2;3Rj#ddGi8E9f2`ei-#5GuA4O(G4bvG0bjuWSV9B zu0+LhPrWygY+hri9=mDI_L$=1U?vbnU0*r>n+T|NXKmuAv41>xSAEznG=)mU?)_7! zN!-zc)3(Iaz^@{2HFh@JaXy5cx!xVM#Jpqu*UfRN&LpP5OqF{1pe0w;caF_T`*K^VRRr{Jo_OnywCf(KRPffRM}8#9qw(G$rKVK=gR%0RF6?a(@Q*@& zSdVX9uN{I!Prdb}$(=Wu{cDK*_iwdlF^M41jL~AuaW;O3kW<1^(sZ0_b?oazGG6y) zKiK#ITRpELg*A9_c9F#dZ7>(|fPO9ItkwK2X{%SJvt(&TB&iU+ItlyG+lsRrTr;Q| z7JV-3nMZRsbf!O6k9Q3^(JPKo3^AvXa~UxBivz9N*dCKfBc_lA^&8>nn_Vzu^a=03 zaiyxVpp3Xj3n2`POBPX!2N|x#0-iH| z#M|OM=WD_(>D`IeVZ7V21{ZNU1L%aKTN|TP_6lD+Q1*g>OKX3RuS_vlN{}}Q;631G zzI2Xlc3>(ub{xV-|1pgT+!@*tC$E%nsFN4Cad}oyXPgkbD!5W*7sU)35)mNpz_$S`{5 ziB;TO%UK@3dING+ZqbW86PKjjcVlQjs_TPZ6wdYe{+rrGk;~LSN;%rM zx-+puHF;3GA6g1;-3GIvnRhYp!7ZBu5|tL=^KHuC8X9cpxVYyW50DuxP_w*X6H^I( za`nVtB1cyQxPBZ%vp4PTp>Xja{q)4C(VlEpnh-o?@4cuKmu_Y6P2n4jqf|}8PX`cA z-?6pBdGKYnhldD9Gc64H87^HpGxPj5%)Sz<1h+PZfN@IN;*Rc#diOYl$`QU-VYqFv zd)70$hZ~NRf1kmAV~MeSj`U#a4yv?PMk4D9b5923>mMP{TuqHlmDatVmD}wIMbkTi zv21kdzyzJbL7>TG2-T#R5lY!3S-WFjj964uk)#O#(VS?=8r;sFfWVk8Q~RtAZ9OE} zw~R^vF$CU9sMh`{r*(i`yxz<%gDg{N)yVludS6>Lr-IV8%z+^wW1J)MKIZG>6cJOVRQp|$guboiFITFP}Jw} zwFxkZ<5%M2W(%U*Oj5#H<2XK%{>erhu9^x&LCwU(DAXo4i#<9z@l$`g&q$7)W3|Nk z9yx+&&`s{pNN0h5pzDQ)*{?Vk3_ge6IBxDdPpFoGg~Q8ow_;Cmeu(&iDD7m;#yK$5 zi8NP&IW{qquc)@TS(5{H;@o9{3~NwQ+6wI$gkurLkrNE(lyA7#%5M_xBomLlEsy9p z(qM&Ql-ZO3D<`)KHX5sXOoxPt&2MT+Oj*&t*Gyk63N(-|OPEyZ10mOhMT{Nf7!6mm z3&_`oMa@6auj7e6{He6kw2%-7w31)k2u-N;PmZUlv^!Vccdp9XM?S_-15Kn@L3~w0 zKK8b|g8d_3i1SJJF3p!xL%!z?qAaz$9Xp`(6Tk?fOsT}qAF@pWmYJ5%EA7L zXEdHHTq|1kTZCM_noN>xFm>O|jI1SoxfcqSG%d2np4G?j?m%xK zcW}CO+SrFO&p~)PREo22L6{~kcUuC=jg%3;5xgU=*L*?({i*3Gw^y7|9C>R&0)N`_ zVM$S)Cv$Q!aIcxuSy>;V6ba!}P*W%IVm`eR<^=V*%WvcrS1>;*8fhH++yVpLW*3`= zECUf&ye$mIil)y!3qcL(6N-oWB`<8sTbZr8ds#Mc+)R~o(0$B&hg8`2s;&4@n(@9e zrWoVzjlF!096oV+#nVVaVl2M9!v+m*cqXrgRZ<8e|D_ty$JyzHlU*lRJ z0|Y`&RTHSpTR>5Gf$r}+2Ta)I+qr45wG#76-Cq10>(qc(jKt=R#NdO#wp+R9Siuu;ghUXuhg`m~K{>)fCGxOiVpk*K$*i;Rs z#TKf#&g=z6$Hsau%=2FAJxdA)QaB5x^V@_6_0-9gs~di@H|A z9k*?krQsVVX<$}bQBvEEZI~6-N{pR!q1wK`vS%jEzX6Bs_Gb&=;YXf-W2e{o5l~I( zM?dIOv?gPm9CY^v{OcYOxQZDIr}8N5ynqd)KE=)rzJef1 z@N?2ZitZqN{pkt+g)w5YIq>zStSt}UC|ezsrmUM&QJYaf5Ukin4xP)dS~`8G`LOZ+M74*Q)U_Nw zn77U!0j#Y%$G8DvWgZOVV#^;o#Pxcw*cepOLA;1Q(zFwqp%+)OuIoaA`o_m$l&OU+ z%b$yQQOxr*Lp`$eG#^;2p4wM0>A@!4assD*y>qEj4>^u51SO@Ru>NTNq9rcA%XNmp zXl9tA@e$N(;`vklt1G^9>0;umt&U^S@4XLokBzuEPOUb=N3f`g8Lkx(&Ytkut0KQi zRcKYm%f{s8WIV^mUe`*jWK;1x4lRxh|b2y$epgcIkPHGhn4j-f2oD0o-#UcVR z3$X9M!Bp`~v~X>YYdb;f1t;Ww= zIUw+HTHb5RIWRc8&#q5klBoN^J>#%&O_Fpt@Q7)lvs6XncL=OhnUbCZ-HpH>aw{WI z#)ZylrKW~uYAGi=HBQ9xJGY*YcO*tSN{BC<-${)lu}hkt?5NRg7x!6IieY+R5fX9g zK8}&hO_Ng+YG!>?sE#^YQsE2@bT|IoOjKG))*+`L)x_+G2t$gCdJEFw2@-QIB^3*r zgWW6(bg>vyfvc%fJabHUw2L3zCt!hM1~7;W{;}a8(w^HEltffxfJ#T*c)9x% zE6mTR$DMFCw3yJ3U?buiwHviyA0o!iLJE)r`f53EZJ1_UKFo;%%9LPh~ z6xgVe{Jj?ECQV)Z@Q>dXY+{b!I#+fE)|i5CCf{T%nJNpR#gqgDffQa+0bDGp_q;$% zz+^#BTXP7V-G>6AO&GF1q6SxzM-R`?WTx!A2iNZwRqT@2yM^nrziz6~HL0`fEv<>8 zqGT^QPyfUqS2+){ACM{iiUcy6mA~J<7&yhgrt$6V-8F!kj0ia#lU1%8*3VHn8UN9{41W@1l9N)_*sFH#J{FHb#DPrkx|7SD%Ca0R}UXViEuj_ zrgdAGZdvU672ewDWz_u!9z+l?wtIpSN1vOY)Y-;w=|YC-S6qzMWk57TGhG_;%<+^& zGEX!mwB}iP`F`e-OdNB4f_gIQFQc)f01w4%iveUXO>r9g8(}Q6A%iuBX*}m?CFcZD z=w3z)i^(m+56Wz_UFfu5)3{ zLPjv7%%@JRrDP)CG0#JMJVrRTFeh1(&3)%3BHFpY=;5aNS{Wy+}271aZLiW654 zN%Gn}mmUsRWUk@kmg__^WyPF*VAC)^M@t zrnC}d$-K5L0koD4^(d?{)ht0J2ScC~;k zRy2Vg{_IW(syOvXcu*C#1#Rm~pR#tnn@5`_ z-1LetoR<58KD73cxyOaOY$ z++gU5S0^z$j$#wlzIZ+!^|n(R#1#8n!2_h1!j$8npacPBm!7e%0CD@HlDy|O-9mkp zU@@qy+;0pwaxVtam(P?3q-1v24jRGPda0yVkdzN(dNl5CO?~{CMAiP~tzy*0d&J3;XDzY}%5Pi>J)DV9 zlb&07`4)#k>T$b;YcW-}V3~wlFd_;S%vbi7>_y-w#K*OPQ!VBZ5>JRKNPt%k(KHmz6L5nMAEwTmG7*_Jnm8yG zO^gi0*{J*ckTT}P3yl_tla`QYpvwol0DubVGE;`(16{j?Dp10|{nmghTh zeVBWH!zgWJT%KZ-QiFZN8G*t69`(v>AY zLa4>OK0bl$*K{|&a#rNO0xhnsFs``(r|3&P>OE{Sl9)8%G_Vu1v6Z@WB6fWON;GtZ z5@&f$=ah|JnArwIU_|UOt5M)RV5K)tAKc)ylJbiI);!4cd>2irdBoneONzx5vq4dg zsGCfSO6Hsi^&Y8WPu*mvMbbQ*^KY9aM?@2O(ZNYCu?~@(_4E)Z-{_3wQsCfNV&Vj9 zK^=zpZ|;~fAbgogd775Ph@+8Sb<;AL&ZuwWOlOeb`tA&u;+Bvpz|xo7Em;tip)KWL zF&T_2!-LL%?XPg#V%sQfpzWpS3z7+@XoN`vFM`X z+z#|?Y6U{1&uET_xi~>%d#F)$)Og>Awq@Y;tDnlaTAvpBIU)#~5p^07!{UWfT3Y0O zF=;*_e~Egq*s=Eq*620HE^A6f0TI*O{E0|*73se1UKG|S{w=x6OCpqDd%nOO6)x2S zmP^Uw(MaP_Xiy%JN75*i_<)Ep1{FK*F(%q7mKxh}L`=LHZ*?dVRGE%=Z`o2VEa>(3 zi2`Dw&=X{jiIfCEBwcn6LLKh#Jza6)v7?;YO~{qf0wQz1$UCk}lMr4}k-DEtKlAl< zHgUB9L7&frpZD5KRP8(BXa_~Q0|M{40v+w25EGd>YxT*rhn0E{boR8;porbkyw1N` zl>{zP!0EB%QD`l3`FWx0sYy_P4qaP+GO%XsQwy zXGn8maSfqhJB>X<3=jr{Zd;n>v7rdjuaUJ{aMw5V7#31FB-$^p@Mlg1m`dkeK!S;* zX$xzv0Om7G@-Sh;dc(LFnBfJC{Fr9})P}!}Eo?^)ov2M+iK#T1u_-h5B+C^9jE?1% zG-n5zK;Y7FsWBMpkK_;S`YD*YUhJVAk7`#f zuIb0r&TfCfy)Bb0W2`X@{wtWg5_%8u2>^f)^_AS?Qs@v)}+d z4lZXxPbn%3*%9$9kc;bcT%&ClYrYfwS=}F6Bp8gi5U?L7>7+yvMM66f#d}7?^!ZDP zN_0n3lRJo`%EZ3qFlbEoQm#x*Q>R9*t|h@7EGS4EO#)qJZ~g&`Vl1j|o6?^Y@hh2X zizk#H0L-nnrcs;d-{>|@p1NObHWMRltAu@DDNWHY7uNvQw61aEKys!^Vy1@m+)9O-XFaJiBsx_c#29vlc%qI~8bOrHqDJ;7lKO()gRF>< znh?}kA6naC;DL=nJ6cP>C#h)opk9Z=vhc{t?*gqXjj(nh-kw2h>08R^6tu$D`Vgc{ zzy%TYAq)qD#e4SoNR&w5UW_#{4{fHKG4}7Qr<1AH)B;nsM}lyF6VXx0;?5bNg;2D% zuie2kvLLfGKCxH0mP_2zoFQ>SPINZTLYyuzd{I=q4!fF%->3{Nl05$SnH`7veBP-6 zQOIA5J|;E8Zy|v{sufd2lWPJiaJ=KKmy54a*kaM?B$Gzo*ns$m-0X+-i%tB)>FCIq z(A=zWcs-8A6QKlRk-0S=|hw_&X> zS>6LL%{;fmGLN@spT6K9rW9Y9N%pUZ!ibK|Wc9Gw?9A-WAUb`lTT`i?68Tn3v%}SS zQme=PW~(R9no-?uQfdXpIuK=7Q-y%T=YOyOMVAk*OV?AkZeAmRB5#$sX46r}H~Ue9 z=rPstHuqPz%1o!N-NXus`CO?{-O)026}#2Q_eaG1v9t-P<|*mv9$8CXxnm|2Vpgi6 zuLGD88a0MYPxjX*BG+<4a*O|0ZV8Fgw=vNUZLvx5sj6RPf^QuF3D)XIPXe;et?H zYrMWpvq6;WT^l#Kd(euPRiq;A5p6gT|9LODaQm-h^;fD`&!EigM1{`yMKGEESdb90 zEkBcyO+)b4%?p}AxP#42L3F~gtHGb8cL34+=D@WR2t)WRU54u|BuekxG;jUpWJaxO zmvY9?@Rrub$R&@Z|GMGZ7>}lmt)I^I)wRNSnv%=M7&@x?eeYEH0&Ze)qaa)xR(X0SL$?@kOg7t9@$Yrm2$H z)nB@1>tu>_P-d8fqnwi`xu%D6Gr5o?S)=jYeX6-~*47+mS@~jpW31kK|F*u-K3Jnr z6T+4m?2Z`~o%+k6U71TGOJmZ{D`#aO&OT@c)O+NevR*nApi4)?D*OeM82J7xhNUwl zz~8Wud^{!7F6*?F*A!-7iZ<>sn!EZuMeCw~2ew6mw ze~=+TMMsVfrJ+hR+$@@9VR^~ig8Ug`4F(sfEbMDd;X}q<^P__NgXB7cj)+y7h`4iF zfu*H3=V4@&RoJ(M@Be1~`6GM=EsFZlF>akWJibwp%%wC_4g``k+zi>8tH1xj{Tr1~ z&hs~fA%vdrud5=z(YhCVR-D|@M$|7bMA)EUU0#NQyJneqFs%q>ztGFnp;Nt5OO7l;Ze;(00j{J3?_G%p zwon@0|CY$z@kLM$)=!+!>94%4wnb=D?pzg{1AMjWIyVP1BN))Z1F}2&@rK_|b%|B)tXYNC46+4X?KJN#r zU2WdGZ|xSoFf9Ku1Pz=odqVf^?|54GGJ7NNaBN?>S_L3$#$YgTQ*640DsOH{t<<{V zoCq%ck2Vg-nm(-9++~jzfO}lS1bi-`{ITdTObO|4w}3CO$67U@$@Lgn(;!3qKFfK< zZ5w4l%1w#8E(_Zd(0=al%fbhrH+q6wH7YX2K_l4pweuH+^!yz2E#8Oy`Fd!T=)X2( zTB>Nd^(P5A)BjR_9EDcn4>dFcea2^B04wbmlFKaZ&(ngw%fUtOGdl$+Hf6& zEkUVUa&gvHBzz57IcXc#KRO>MK+ElizO`x(PQQ8C!Yg>2V0k{OUUO|cb-QT&W$)3C z4#0jJ1HrdliSOQ7V|dxxIi%%LnSy>0aXrYD7_Rpd6VQe@{rDyh-3!w&-zy;_;A)dc z*-j@`!kWlvxwp0W8Kim9m7Z|tVT4{7U-4pE!7ii^@k4N86PEKTB-=P!tsloQ4=Rf@ z&K}M7@kIz(Gw9#R8v>n`gi`)3jl-19!RIC}z4fOhjx)fj7uW9UVW@Ncdd~0;W?O;B zJCR2*;41aKq-PKTw&Dp~1;NkQ8?usj#=#_k*LMADuP~gyz1hQl0T!}{)b`#dSk%_! zwVNU-i;rgEnegV+_JzK}T^o7KBHu|2M!&QCFa3wu7SNr(a6B!Y`X_Wh<4t_i!4VKz z*y~N^N$-odzDa&DT-OBJHrgrzNs$PhTG~y<__murGe=Yvq^ZM%mtqPBI&m&A&X8GbroN*%l)j2Hc zlh4p2^UV;KjGeQz2lf^-p2j=&AE2}H=l-JC)Vb==y{UJGPjdwjAc?N#(5-bwo41Cr zz3R)GUGp_^4mFu{{DeD;!*TV2%E$G7%iADqYt2ywb8A4USM##fC+H=2$B)fBu}!Z- z;wC&UA;);^WS;MC3{4rwRBbOt7wyJug96Mv9&7kKUM%qES0F}K?+asZPhq~%&I+}L z4A@(=`FVdLp=(x*=sv?vTirJ%sKn`%_1CmN$HzF}>^qvW(n#Pfxp~YWP&~0(>&06h zsv6hbH()CIabk0&n%vyJH2)5NrRXv8+)sP&ZvE};-qh;j!%4%ZZdcBUr>UU2%isI( z;1PVWcsMR=BEuD0#q|}-s1dQhw9EpY6MJ+rDmm@n<@TdzDje;!}Aer1a=f5vgV;=6B&ZEoHn zsXr>p&+LEO9e#QEVsow)&-~wYmz7}wAktX9zXoc3vwujq{ybesc8`DkGP<(fxj7C^ z+c>~4bPqB0@fOW${ed^doi+rRde7LhSnl0=cK~H}W*X)Xf8MFrHZ&?pQ}%k#=4^Gv zqxG8f;4BZ(wF1SUxJ-XoY^dnYbOn4MrIPK<1=OXwbh}MxprLsWVcvHKMhoIr)ZLTm1eDFUj(uJL^RQWg_h5u z{|*hcb45vat2R;*FbvNN zA+(LpxPHGXz4cGkWv5W;0?!$h&PN~8Y65W4K|!o;P!n?E*{TAcKE!+*_1jWX`g3-` zx8u}Rys-FY{7dxh<9hN=y>D|=6inb_WX?f7-eJmkuWW-;?{OUVNR7Fqk9EWeXodQ6 ziW(wgE3Oroi3m*AmfNYQQ#bE{gs@d4>RfvSN?UQN$7E&MLuLI5_w&trrg(LRx54-J zubX>~>CZFBp1yg7H3{bEXgNufeNd3b1Hw3N9C4n?Sj@#$8*0M5~3_dl$= zaK7Df##w8Fe@LxFpW1WBJF;Gtq%I%ZlQz*!UbzoKH+ktFJd$X$`08%$xj$@pw4ZWq zFTuPTTxmY*ji`V<|236;#DWtQYAaN|+#QlAxYo)bzw#s<4z7d3t|w0L=jJt@Blj9Ff{*XvS!&}(vI3pB zVL@2+ctz$ zDiH4MIz}88mHy)>vWNZkQ}^cWXp(@-R7uFMSq@4=8nwAz;PYHR`4+RQ%1|?naJsSV z5GTyqEIZR2Zp{39=rhNl{)Q*(3>~Ko$NM*{pw zgS}+A*X!7wI}U#`V@ENM-NMW_R5sogbSj*ezPmuAP5n`n z+b7R9hH&GS!Nb{6u=XEY-6v~SpNJ~9YzbgLY!IY@Uf{00!Qib}?Q`7d=Oi7z>3#RO z*vy?enotq>^|qvU0ln!#&;d~Kd#qj~4>#8Qv+M5i)Q}YdWxu#NjQj8uJ^5=B@5MY# zuAWAtFV6*BaN)Pi+A@>W;A{47*r)pg*W)Af6%bU*`Hp)F<43&tl+zz%4}XS~z;Y{E z;%0ztBNtQl{=x7^COAIk+g3HVgETK!yvU~G)(%Jb9<5zT9Q?yw1UdKUFgVw>H^3h7 z4ib!fx<@)CS3|95C_{JYJa>rgboyQTrZ?-U#{AvmLbG|hA%>&%$nD~+Rn108ev@(a z;qTMw*HLm`u{%u58jM%fhEkhWbHvhox19I%o~-2Qt;dhHR`ERq-Rl#x(Tat}7=$zk z=Xs&nhL7HCMQQV+12efDbcrroz`8>hbj$OK0~w7>%N1FJ4cyLN=R&Wg4ngg8BEN8H z_~W$N37>J&r|~w|L;I`=**lOIvO*oI6*ER1LWmr2=_-jjt6)nZOZA@Q1wX?S*SNia zpxGJC^H{it9lh}Ag0H@j`>mVp-hrM=XEL zAD)3Z9kUTo?(4NtJ(U?xYvvAIZXu3Nf>D%K@AK@$y8Hd@9>?0RJPgd5`Nd8h4GbIL$Zccu>0CI<;Z&a(nfso1|d3(MF8`Bp<*WLl!R zyQ)aWReuic!>%SvI2k(`pXzu;#eGYVV1A}6*Q0h1&)o$yy1FZKICyu${Ts(5Rghs9#scZoCLPWA1*P2_nnd7}TKx9%;SQ{{tk>cfVs8}^vq7;sy7m#gsQ zB&y7;nO}Fz@}0o6TpNx2@?C`^b?O_^oYn#_-0~a#u$JZDwhq3@noO3eDTZ4F=Ge`? z=O5l_uI_DH+fm*EW&(2mfVbeZ`^a82Ao<$5bR-Bto`8Ss7t}(s3dGDSot~Xv2c542 z6z*PL;?T>9ukkaM&p)0XymTCL_3o4KuX^TCJsm-|hnf7Wr6;4hwP~3cFd@g7;*Gy$ z)Si11!sehHH0X{Jx-T1)1K#RXW@SSb4sQpwZ$Aj?gm}t8joPBD4t+(K?F@R>h5=SO z^O?x63h$l-&HqSo_ou;r-P+~OFTcOf4gFRe8b_>XC|!4R$xpK}&a``^)wHLtwnLGe zFZ+@H&FXh_)_3XeVyYix>dB992@}b9FQD3PCKeQUEV@r)U{4ZxuhPBN-kf&=&YX}O zVCzcl(0E%+LHC+IU*|(8DuxsV2t!ApiLRJfE!P z0-!SXU7Jy}<5#r2i9}YqySBIYrb|dX5mv^9#>nEVV@Q;GRl6(0Sx@8JwUYgT>k<a|h#&eVEMQt(}FT3)-PU$7)9 z=-+pq#77dY`>C(wxOF64n6nc9#I-`86OvFUJs8*dsaF}Rk`}lIK*hfTa=KuY)gy<4 zZAL!^Vwae2k5BHYuzrplm9374gTE++cbN_Pb!nM}rNRWdh|WniMt7lF2FJROI-sP< z`KJBl$(Z9$zh2vObFU&(7%^0q6w4KEeXs7y1yq-pZM$?B7_P8;H95IzS2tuF??23Y zq#-tO5pmRq&u*uHn-sn2aE)Wr?YA`f-E8hOgN|b^`Iyu##D`#}7m^;B7Zr&G=GCf* zsU1A!Ds=K3Oh)inkHH)+=Zr%>!PnpB{ApB<(D6PIZUb1T)wmU z$>%H}bPNmgJ3E^OqXQwT(8u(_MJm@Vs(fXg&HShG+&}M?@k_!4sxEEyc5Cs>I6~0V z8{{UD>rHZ*tIW==j6dV?xSeT6ksc8*E;NJ>9%#%BNeGLJuZ+04j)-zUh+l+P z{p|uQ;q^bth&wx3yO-~9td}0To3om;SJvIBER{8QB%>Q@=~qrL$N8@-&Yrs8&pay6 zsL(ticd+SVb(I$+IGc+}6S5{FmGABbGJF4&-|l@r)ZC}SdU(7%JuxUO#KJpYu>AIM zYw!pOv^lifL**LINv)LOWD%4y+v+I#Pl#y%RM?F|8nr3i)!?1xV`&cc324a|*5ftn zT*G~ii7GWUD(}UVvmu;?yCSEy7$m7H;1y3*>e>{xu-;(7BS9avB!Fz|O0%n}IbrrE zLx~p_JRCiCfFb1RQle~AE>fZq_tP~9j<$xlY1F^o?SOv|W71Z4&~qoeVoJBmw?9#K ziu^dP7wvC*wZ!AizSR#Xs7VVQSIc%4Lbs{;Thgi!^8!=1+k5xxtrlRIyvfy^x<2f? zO~GsxqtHusTe};aUOn(1c6i;NYZjeu-{w#f)BFt+IQGEjII~|bwHaHMRx#2o?umXG z4^hk-72fqahgtaoRY9~$N(^eH>KeE*h}4UhNAya0_($3u$ik(h8^dgi&cnpAnP z=w%-?sro3TW-P4(gzj1lE8VE{O7N14|e~a%LcVDmb(dMplvtvzm>#z2R z2fv{(RQN@ol=Q)Ueo;|I2zC8^+**|-F3{mWF}DzM;jigRWPLB=s`T!Rw~mnRF6*d# z(_19YE!t4q8sLuq33-YHGd8Z?nHB8pkxUBr{Edex*h#_uX#AB{>z1s$>eadHagki{ zZ60S?YQj{#K<*_|%2`g)*8RUR2bZmqv5X3{eHJ)lX%t1n333xY|ZOP}N5orPWoa2n0?Clg>; za&1sf>Z@x0{K0j=*>bE%waqOzxi~W^gK={;=)2L`N#jly$a1|Wl9Yk>T|Mb)8ToO? zbKmvP`asm^=C@&k7~l()?zAQR6@DoQjp3&Ho#h8a6y{{dQ}O5RrGpkAZA~`y)0gw( z5^SSF|8p>}2Ny1Q?gurC&7qiJLU1Sh7k(WhGvS(&xc`K&=~2JJXoY0ss<#p!4K~{=n!kSy>`iF2onZYk8zZka8xlFYZ!=p+ZgrkO3PXlD zmd?62^s2W-fhOg}U!zu?vSvv0p*5Iz`_mrtQc^{|e|0N?qPu&nV2Z z?m_n+O)OiO8tgBpGMJC%un93(xh0@e(bnqDS#ei|eOjf|r*c&%B-X#6b=1PqRT@zayT7yIt+0=!txZ%g=3AASE7_az`HcJc-YCe!K4PXujxS9ieP z*afe+AlWnafm_RbMa3n)Rysp84*M#V{uWhbRmcBd|xYgUA>F*w8)VYb0#FLAxHG zUNRltsF0pM`TQT>k<@r8%1R!i<9Mln6DQ2Pd8YdPPd4qRxB&7=8a$aoaYSP5JU6#H zqI!Dkh6f{vBi$toFtjiNgJYXyR!@D}cE`%V%A-xWCv`XmjKbSB$7cakkUg+B}!gYHP&}Tl`$;QAxQU!T?Zw*?FJ_FLi zF8MXB-=i#Ht?ORl5r@2=oDN%t^1k}|`c4uE2>pFQBqk=-x(plhPqBk5y<);YmkB8U zUE}|Mg$eUZr_P#Z%%Xn{mPo8E;`{Ek%bOkxe_YVs{fnlC0{gWdi@@K4WpJbQ@N#^q z|7X|*+30jW|G@$jU{Hwv(}8$c?!S^N`2Vi3KZr8^6BijGJy>?+M3vvH&JCv~3osz4S(!!w z(PO+cDXP9QLraJxrqw4(Rc#HhfAQ~H_EO@xW`(D9&j?PgEc&>b`B-&hb0G_iGetgL zYfBC4g0QwK$+w5>VgY7siK% z0awdrITo)9X{<5YP&(oGL2DlJ@sHUaci7h~8Ri%dW6uxwt^YDbLB6RbdL4CRzQ`Pp z)Q^W)%yVC9Y0T`XJB0ugFmQFGqO)A))w%pczI>Myy(P|B__TXTd{UI}j6dJ6 zIpMfe`nd6mJAFd%P`q4PG$LxoxN;+DpN&sp2XQuK!vIm}p`%j{0a><@qq5QNEk{{yP0j$D_uas88d03n(rVq6 zG`EwsAq9Fn)ei>Lt&5 zy3b;F_V@h;3wb&Q`|m~z!xG`ZMqn|+zSi{(r6Fs*NEEilk5>V;Udsn7EeKMU3H{|G z3?z(|^Dp0Rr*;1$anqCBi4~IW_^1o?GrME|Y&bE|_+M~JB&Vhhjr`wz zSccOKo#-;S{~g>PSIhVqNK=C1GZ)=Q$~OO0ko708U2rOzr;)EEsz0?|T;}BF*p|6e z^|WlL^0x(j!?k^6vd-E!`1s8a+(=75%MDj0ZEVL?r zg-sixec6qsrZ3*RyQbLNy?-2kBp0`N6t`yQn3_bkDy*WW|6!=_3B3zdb2_OoCp`0k zpSA93BX*ScPL@IK+CYLS$JS$mYu2TR@g!KqS-KHij)y7Ev(RSF_k&<@H|n+2Fy7b~ z9gBpfb*tPZCtOZxTDVR_M6?1I$X77+vyQnf-eOVg+E!H!AU)>JqNHm>T^w!{X-m4T}*+nMzXuP4Y+&k_+$e;QQ5;#3nYI$8%)6 z)!6M={K&*}euUTL=1RK|v74U#xihHtdut~mn%F+W1lii6``pHgI%AlR@^vIs<$zLE zMQ%Uk4z{2HPwkKIo?d!x{*Ih30;scdCtD@xBN^^U*Re!W4vX~Pf7p01v*+ELo1*R~ zh;W;gzJc|2pH;5rNE4IUQOU+GEZt=6WRHAmmFLj?P*=(Gr@PkWsq=?j$1B=aW~WHYeI&Zbt~4a5&Elrc z^(LC?kA)>|HT-}?6ZLmVyFY8f$kh!X_yk*8f6TCg5@L21&uKk;Hs2y=P(oy)i#i$g z2+3emsv8yES0{J)b*rS^g1@{!37I_B+s8trMnj5JohLTD)=Zlj-J*=>0OdU z_f1B^AV>RonaA(_3wW>y%;r}T4fVE|8A&BhqXafur<>T)z#wYx^VmnmsPm(jrX1hvYvX_g(d^Q{xX4lDx#P7FPV;C^~@cF2{ zFj?04f_3Ih^HRHRi7jqd%W)`q6~cx!)=Ih%(nx4sW};`3P9P(WqE+=6gHODIPKF&hn{H`4A^1N6NF; zK~9>=b9HiYCt3U{+(cM}9f~(KBRvJutJzZcN=}zGm?S=a(`TQlZ4w+6)~vo3K$_9} zj_Qr;yGH+f2(mCf-?jqlU)m z+GKO1yY|Phrr7L94JEt<%-DmXMmD#ubxRv0c;QznJtC$heVmwhswKw=zMX4~{!>_{ zE~oc574B8$KIO!jz9wX%V3{aM__L34FCk4elB;Xv>F+E6v$-xIsS0 zCkLpGa<-osW?l1&b4<#p#)_OSz3S>OD})v*vv4#xlGux*-;_ZuV1`MIH+X)M3o{;{ z1EtE1iuOOEn}C$PYfKpA^d5~&4JhG1puN2Uu~Cy58*%m)7%e42yh zKR99{llmCDZE%zKKwcH^?|Wk}4Hf?TXuW;MrX94cGW?XQvkHR4fVvnz3qxrVvG_DM zqQL)H>SF?y3afx%D)=^K^j#4*ty+KVQWI?oBgRVk*(M1q{C*Oo6*H+=m7$vq$|QQ7 zjCBbguBE}u=g-2REIPQxVe~U3W!Qgc2$EJ-__6w?kmXI7cw(JDi-3nION5*_B6W^C z-BcKlYrP>{6gceL!U;j@uU!gkSqs@y)}uyDi7wcYabk1n5E`_etbLGM<)1@s`Vy#oF5@aoVQq>XDhYRJ{!wJW2VaQ?-*7)zJwom28 zOH>}WGEuh*6_}?XB>q`(dZ%uSuYc!0HLQ^=_+Hn098A4il@e=`ZP9O!vXOqQZ}aJ~ z)OsWwbCoj}+BS`>2Hh>bdG-}v8XnIjOwq>n2I|a%={fqDxt`46j)vCLi1n4f$NSr$ z;R9Wegg^_Sxa!$xivO&?BY}aI3xPp?d>o`g;^rdI;cU?1A$0jr0%041K1X*e16f?w zNO+C5&|AHFgCd|=9rc(X+sAOHztY|1Xw%&IN2tdOS6Ly6A~+vV_$^r|w4pT^FA)MKAjeuBpu%I<)- zP$0LNW<3jhgxX@f6k(x$W5!H}D!iPrWuVoWj3+T)nV-7q1I~I_#e2$vr*uC*(`Xyf z(BCywJ!PdS=M=WIrMIe~+#^mHzQGe4SClz?x4N~i6?d{~o@YNjk@c|k$zW$2qJkv@ z`GmTaQ!>wV2#~5pd^S@MHZAlddP!!_vyLCg44O9#Zo~F-CaLrw;!ODu+#iz-4%}W! zP7nnEA9T^@>fCkGo1cZadt3d&VRLAu-RLFLyGFRdvgIMg2Jr4LGt(X zxjVfI&z|t~gA?V0g7PYy4y2^WPJM4o63Kr^5aG8b|K)$*mF=&c-5Rlzc5U9>}Wti!{ zq>QtU{#|PSsTuw%BHY&O2|c_;ZETp}kJMKB`!2KXK|TV$5j@q z(bnznWws`U;_mmT1UZjpSyFj63UaCA@ca!k!$p~WE9m*=7=vF+ZV#eU*12|?LpYRi zq?j;Gi@W@8%%W7RFn{y!p~w?EE_okfW*+z}65DT&k={7yf(-a8KPj~c&@MgBX0b9? z``2uauiQdL(eFO=AIqHbww;A|_Rm_$4re)LvSB&j4&b!))&yorSl{-6Q>Uud1kO<1 zv6vX~c%Ft-=>LHvlU%0XJO=NHe$3W%g(J3|^{leA`)eOVN}n>b+O@{+Qko4vqpOq? zb~-!ojy;`+v__-Gn4N0y%MES`&)|nI9ab#}IVgKRy*VShIU~{Za zDTBp*>bB2Wpqy+oOw$?m*{}9NJ}}?V*zjve%+q~}nXI?R`>2;u)@@fY0LO*t%RnF+ z$HVAqo?Gee&rg&nhy*%#Y%s!}VzOBeM=MItN(9`Sa^mMog0I!zbVePnge}D>oZ)`G zOBZNPEbBO8n>jF~b?3iYgulI>Ldq{oU7cpDZnnWk^)n(? zk*Ga8d#7+RHOjYzrC=&e+@Xo>zk7UJjVeoe5oY&u_2DCY6PZg+7`>d%8I-$z&MSEC z@Zi4B`xUC?Q2Is!%hv-y;Wgq{<3Q#g41jTB}!m@b5zGDb4*2Xo?N~B zT#DdV==Zdmk=@7jT7L73%uG=8v9|Bjy2CLVW>GlxI;C3@FqwXR@REe>Dw_fITsK;j z7^!Y2C0K!!ZBk?(q^-{1o?7Gmqk(4AxE>{3=pskguk2dk#koFfrDz>7mXwZ8)Jr@3 z4NG~6>AtpGU0=g@a><%sxhV{DQ}a&gh9HmcI|lt~S`ou2?DhqBGaOdj9~YaV@`dm(P2_*LL+f6qOPau{RAjb!rGv zlnD`qT~Hay{|t3jN%QlYk<;y4e=EP`>#c8%8>t)R)q8X>9V_30NG*(C(0pRAbw-e9 zk@JkN`}JTf$J48J-pJRU?~7Pv z@w|P=x^~R7f{NbZ(MV4+_GWUN4OGR5l1W5Bw-FrgI4IaINz_Y_!};N>6|mKqE; zS?E|sw8b3|IXIE>#&{3)C!)kNeOY5XnyK2PLkTHWQA?DgSs0-n+lJ@4o@6nh%_w8S zzjmLLU@vsWu;E6L_4T7%oetZ}d$ANs7GPbB5JN3~o34pIUl8vV zA3J^$xEF9hq0yBzc*~6{F)%VD4;v}6eT^&==fS9K7lFnqUxH(0$ky-8*}Ys;K^Qw zw_6mGIkItYj?TZEn40kZuE{H0*>R6!sRmQ|9%ZV#OeLR1B2+TVg(xpa!)4EvYqRmR z?o_wEKDIQ^Ku!*lLyG`YIggfA8$5s@1ct~P8eW5mQC#{5a5<=s2n+mQyigA9wlRlb zBE3LiIzL$Yj@`~w2B#>LQW0ZuRHlM`%Ihy9pWb1On-~->3$?8Fxw$kyod^= z?_+JzP8>!3JsmrHjq`~jN6zCMR50ZuI-ZAbpw?wz0N8MK?JATi1Ea)2f{p>WAfvW0CRAgty0S2!jG_?qr`-hQ z(h+z&Us%&FmrMGJd)0Eyi~FqFkrb2i;z1lrSGgf%%erE$wI)QuPOU!Mj`=x1DMOns zKXE{FM2DKdUH3)IdA9oVB@J-|>>v@FCGqE3Q~T`dH1O=tjf4TEFZr^4x;eQweY|dP z^GtWq#DdB2$0ezd{~-b#I=_tfu|ER2N4l9(p$b*FQGOM;8(|izTQ06qT_1 z9!8L~HzkH#YBIzGaGn-uas3f}v7Z|_x^icIO-o)ke&C2fFN=MhK(5CtQQ+j|75BT; zm1GmKpOKj_W?D!kAAmzZ96)n~JIN!vP$F(}Vn)o|E1rpqOiE7fQY4Ffw;-~A(=<97 zMU?Mf+2;rpx0>Cz(eEp2RS)j-D?;dfgkL{J5Jn_0F`Is+l4>$7Dk_R+Viw(!JeVK*1ZWAV_ZgmCxwCFN z|GY0HQJr3UqfUc_QvH}VR#{aW@cLSviOpoqhXc8yt*sf}^q=9TQ9Icj9Vnnk(dl;Pkx=3~{y2mv(gXDC z-o$)K)%dpXW{$Qkii?YhyV=ClUAQoA2L)aG+idmP zf9bLDce*j=5CWz>*_<8~q*uT-&=#Tuv8 zYO~?A(7no39@oubOX8FD{EF*(7Jcht$Y&!zF4_#2N}bR4!4&~IPq;gAjgHrZm-ODD zFPp>ODvTipSlY%=bI=98Ik zPEDQIzz9Qu@s8+cGViig0-ntra*B$U4;>l~vg?6TX!r%PRIf^6xJGYT(?7r7$^)~* z#3Ktl_#jsJ@^>UA%?jUayrOQo3W0h$#!MGr2nqWRv*Z=3=%h+7JIr@^6aUB?HCfHq z{r2YU6}0{>D)f%Q_kGUn_1j!$A%wWVQ#=a>nrF3xzf>DANcfIUvx`$TLD-Mz_VYHD zl=|cHz7^J0ep#%jFz#%MvGssheJ7K66FNDVn~Xx#>`~bi=)3#^Wvj3TaZz-#tjbJs z;m{zROn!l~qLXMmq#S2<{6OG$Gl)49aXwzZ$?KiV@^n%JrJ|f)K`lS}H4qg2hLHpH zZ-`p+d*1z#%OywW0DpSqaZT^!d;L(!r_=ZMo+!a*Fe4*K@#5KBjlaL2AI#}mitw*G z(;*l^L&n>xjD)t3ci!$Ogxhakzb9AS7ycDEAoU0IS0RFXpp=ch9C751-nY;_&#kyq9w z>cW^a=_DaIPeVk_E8DDjY)Wn=KR@^QHh$+g?AZtE83M?;OD<+IoTe5&DP~NyiAA>m z_fVoB36fA1j~yhaN3s!=x7i3M<9YBN2(dAcEN78`G1)Ip8PHYAn>9oMX&imGY21I0 z+N~kT(EKWJ6uaXyuXJoSA8u_SSS%(5#`VI3D3{bivFoUG;X7u;;5pu(%m zARfzThC9e7Loi?At=ao-OIK0&Q7N#ffBurZ0OvdOm*IWSi#cf&gWib?d>DbUn&9-l z>WpM_QfJMZPyLw)7579PEfdmbev`Jx$>0uS_O>@-rzjM{(Nm z{ch$0X=rp-`kWVmdD>_@M5;-%pM`t^4LO%Qf{~qMOT9jvmf~)c6%tqprs`!g@2aoQw<u7 z(9YYD_6a7rs5|%7I94!iz}s{kLQ*=uj-|v+cTRS;`t72!MvZLFOV8gms^J&H+U5bT z)u8zeT$KI2-r21XKd*{1Hty?B1 z8tYIjl)Z0|@n6qt;s_YY^(u*6vK)HkJncV4RTnUTz(;R?vl6b&YB{xC4-1X`hEPTM zz0rjh(8c%)Jg#GMg3QI+U|!AP#hf3)E2n3@IgeV~?kr>q`h5}i4*GC5#)-cfPI?7Z z;OARKex#iiVv8xkg;!4emAs%>2w2P2*%;GEqg(078}3R0@%LvX{{WtW!7=xxN(lMN zyIYmDuzFZ3DrCp~CeC4%SGxl9pCnycj| z?Qih7$SRYD{eb-xVU?cT_#>Re9fzF=F0cWLRLxd9e1wmc8{JMW8#`Cvxx6ic9LPN6 z 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 literal 34615 zcmdqJWmH_v_BV(G37+5%A-KD{1&81s+}#~Qf(CbYcXyY@t#S9p-K80F-}~JEedp86 ze3&(B&gymQRMjrqyLO$utA3|L73C$75%3ToARv&XzKMN@fPhhifPl(@hkZ|J#QyF1 z9(-^Tky3$wzr5j%L*C=K&f*%*%66vCZU&Af5N5V^HYW5=Mvf*Xwoc}D&galw0uT@+ z5K>~oD(>kgYhK>*w;A_u>ih9y)T}84!=b-^eTadFO@I#>=*_*YR<*ENtE_iB25YUg zSrWH3v}t>}mHC`3w_2?{&!6e8c&Q(I3e=MzV1_%gU_?y)U?LS7GH^^6WdT{CJiao= zp~a?r_?nC+I0=A8cG6wAf6`m{)l%u9zag5+n2kjC3rz%`^xro_s5x16q5nkRGfZhe zD*pa2iZ3w#=lJ+PCA@i%Jll~x`KY#rWYxvtN#*l^metAQ8e@eim0@TiA(~Wj6fvLw z)fi|c8xWpLwF@$isRT|qyZl1iW+Nq+J@-e{ZAA%}5>a{YP*EIK@xO?-+wpJQsc31U zFI3=}X$y|)--}Jp&#}@%&3^kj53(45i$prSG}2tN`o`YmdILA3miGHgF4Hu}9#h0K5V?tP5?bX&KNLu|N( z9jmf&kX0Lkr+Wy@3(Y|j8-YQP3!5Ia+UfeAvl0Gjv`)->D9*{wgGaAbo|86+3eB+b z`Z*9(aiXZmZD$gGEd%wJfB)j$`)#7`5bv{)_tjS+Oje4M>+PrsXL>Oj=7C8?MS<0S zM4SJaJbwt=i}_y4pT;z(Dk&<`ZN|AgiS)5N!^co>S!>0@)~0CXo*GptPnZw#(Dc3j zEQA^=>0M>twKm0pFTa%6l?UM!fX{g0zb>+>vPtlSZNrO1(Skc7@&GFb&KX^{&4zTG z#OA6QL}`eDEGZX^~dukB4c}a z<^>DWR;X>G?~yf+EvVIIYIT>_?7I+DHr768{5&kQOAn7GmkXw}9SPn$Xp|icT9Lx4 z;sPm0DV4KGID8}f3uzbXmBu@JEp;H`{1G8NHP|ySNd_fPPJ6P)V2bbh_GaM1{e@ok zuQb~Va=+E@Uazi=4W-RD*T*fhY;tV`*1H26oIBKQvApvb>|`exXAG9MA?(Cw)5Iw2!(OH%YRBYbBZ4f_pgD7?I0pgWuk z_iCy9u3{(jK1eoY2=DM=8Cw*!oG65o*myNwy+ooOhUFVgOrc#oP3@~y9|d*o79Jh8 z3a88EHn;K|D&1ft?#Jvdyb(M;Tp!z2+X;!Uo8UK_)G(Xy^94*T2||QCIiLn9&7QeW z_Dda|yLrI(zcj-I3WZ%7z(v)%fF#`!j+!eN?yap@eN1!|gC2g25-*{U297kW?!q@&#b(z5zO}~!fTx-M0NB;$UW0gHVkJ{0|1bn#`}b1wqJj-S zdD7CZ!zjMnOmy7=OE94HXMdT}mtu4P{$BsYMqP)f+<=)amy0b$$kts@D1|P;0P#gm z|CpI>my~!+B8dn*Gg+>C#_OW4u!xdkNkek2Q;J;%SV~nus-}l166r`&!&2(@R^MBv zuLO(g2K9qf6DCD3kSXw0{wWCRLtbBgNdpdR9rd_e-n|&{bi1MS#I@*-`49N#ZW7TR zcxDxI=R;%AevA?8)W)gdRBW&~%U#`W!QsHh#jz=E)?=^k%Vf9{6U47Iw$qIX#IDz_ zoAno$GacKE!!Y@(DYOYiywZ9Id4&(*E~#B|H^c=F5F`@%qe`$?3QlB%x*v}P?79OI zUZ^|a_Gk6R2V+V0SI1^)sfkuaNx#69sHe<0$CC?i5h*p2lE$T8iK}ar+VE98_#(3CkElS_S9EG-lCc1JkhT z*a!8c(0WGjkw%VGDnv_ZjPG@6Z5rRPBH3XZ_hvH(!l^!(Q-86KIqY#2`gfD!7lD?%0N
`|Y>FGiHC3u8>X)CS6tHA23Bm>secq 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 v_8R Date: Thu, 14 Sep 2017 17:39:44 +0530 Subject: [PATCH 016/135] Webhook doc correction --- frappe/docs/user/en/guides/integration/webhooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/docs/user/en/guides/integration/webhooks.md b/frappe/docs/user/en/guides/integration/webhooks.md index 30d90fdd37..ff7df4015f 100644 --- a/frappe/docs/user/en/guides/integration/webhooks.md +++ b/frappe/docs/user/en/guides/integration/webhooks.md @@ -29,7 +29,7 @@ e.g. Webhook Note: if no headers or data is present, request will be made without any header or body -Example request sent by frappe server on `Quotation` - `on_update` to https://httpbin.org/post: +Example response of request sent by frappe server on `Quotation` - `on_update` to https://httpbin.org/post: ``` { From cbfbaed73e4717bc1668f4877e9602c5b5bee52b Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Thu, 14 Sep 2017 19:25:38 +0530 Subject: [PATCH 017/135] [Fix] Codacy https://www.codacy.com/app/frappe/frappe/pullRequest?prid=900990 --- frappe/integrations/doctype/webhook/webhook.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/webhook/webhook.js b/frappe/integrations/doctype/webhook/webhook.js index bff8591592..3fc78e0d2c 100644 --- a/frappe/integrations/doctype/webhook/webhook.js +++ b/frappe/integrations/doctype/webhook/webhook.js @@ -3,7 +3,7 @@ frappe.webhook = { set_fieldname_select: function(frm) { - doc = frm.doc; + var doc = frm.doc; if (doc.webhook_doctype) { frappe.model.with_doctype(doc.webhook_doctype, function() { var fields = $.map(frappe.get_doc("DocType", frm.doc.webhook_doctype).fields, function(d) { @@ -18,7 +18,7 @@ frappe.webhook = { return null; } }); - fields.unshift({"label":"Name (Doc Name)","value":"name"}) + fields.unshift({"label":"Name (Doc Name)","value":"name"}); frappe.meta.get_docfield("Webhook Data", "fieldname", frm.doc.name).options = [""].concat(fields); }); } From 94e216ba8a324983b1d5fc30d1e0453cb6c187fa Mon Sep 17 00:00:00 2001 From: shridhar Date: Fri, 15 Sep 2017 16:35:55 +0530 Subject: [PATCH 018/135] Email notification on docshare Send an email notification when a doc is shared New notify_by_email colum added in docshare Checkbox added in set_sharing.html --- frappe/core/doctype/docshare/docshare.json | 52 +++++++++++++++++-- frappe/public/js/frappe/form/share.js | 3 +- .../js/frappe/form/templates/set_sharing.html | 2 + frappe/share.py | 21 +++++++- 4 files changed, 73 insertions(+), 5 deletions(-) diff --git a/frappe/core/doctype/docshare/docshare.json b/frappe/core/doctype/docshare/docshare.json index 83d837b7f8..7c5d56c4af 100644 --- a/frappe/core/doctype/docshare/docshare.json +++ b/frappe/core/doctype/docshare/docshare.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_guest_to_view": 0, "allow_import": 1, "allow_rename": 0, "autoname": "hash", @@ -13,6 +14,7 @@ "editable_grid": 0, "fields": [ { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -23,6 +25,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 0, "label": "User", @@ -42,6 +45,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -52,6 +56,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 0, "label": "Document Type", @@ -71,6 +76,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -81,6 +87,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 0, "label": "Document Name", @@ -100,6 +107,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -111,6 +119,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, "label": "Read", @@ -129,6 +138,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -140,6 +150,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, "label": "Write", @@ -158,6 +169,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -169,6 +181,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, "label": "Share", @@ -187,6 +200,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -197,6 +211,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, "label": "Everyone", @@ -213,19 +228,50 @@ "search_index": 0, "set_only_once": 0, "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "1", + "fieldname": "notify_by_email", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Notify by email", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], + "has_web_view": 0, "hide_heading": 0, "hide_toolbar": 0, "idx": 0, "image_view": 0, "in_create": 1, - "in_dialog": 0, "is_submittable": 0, "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2016-12-29 14:40:40.284335", + "modified": "2017-09-15 15:58:34.126438", "modified_by": "Administrator", "module": "Core", "name": "DocShare", @@ -242,7 +288,6 @@ "export": 1, "if_owner": 0, "import": 1, - "is_custom": 0, "permlevel": 0, "print": 0, "read": 1, @@ -257,6 +302,7 @@ "quick_entry": 0, "read_only": 1, "read_only_onload": 0, + "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", "track_changes": 1, diff --git a/frappe/public/js/frappe/form/share.js b/frappe/public/js/frappe/form/share.js index 828ea69ede..a44a61eeb4 100644 --- a/frappe/public/js/frappe/form/share.js +++ b/frappe/public/js/frappe/form/share.js @@ -140,7 +140,8 @@ frappe.ui.form.Share = Class.extend({ user: user, read: $(d.body).find(".add-share-read").prop("checked") ? 1 : 0, write: $(d.body).find(".add-share-write").prop("checked") ? 1 : 0, - share: $(d.body).find(".add-share-share").prop("checked") ? 1 : 0 + share: $(d.body).find(".add-share-share").prop("checked") ? 1 : 0, + notify: $(d.body).find(".add-share-notify").prop("checked") ? 1 : 0 }, btn: this, callback: function(r) { diff --git a/frappe/public/js/frappe/form/templates/set_sharing.html b/frappe/public/js/frappe/form/templates/set_sharing.html index 0452dc7fd6..33e19c0139 100644 --- a/frappe/public/js/frappe/form/templates/set_sharing.html +++ b/frappe/public/js/frappe/form/templates/set_sharing.html @@ -51,5 +51,7 @@

+ {%= __("Notify via email")%} + {% } %} diff --git a/frappe/share.py b/frappe/share.py index e277570504..3ca0d13f12 100644 --- a/frappe/share.py +++ b/frappe/share.py @@ -7,8 +7,9 @@ from frappe import _ from frappe.utils import cint @frappe.whitelist() -def add(doctype, name, user=None, read=1, write=0, share=0, everyone=0, flags=None): +def add(doctype, name, user=None, read=1, write=0, share=0, everyone=0, flags=None, notify=0): """Share the given document with a user.""" + print notify, share if not user: user = frappe.session.user @@ -39,6 +40,7 @@ def add(doctype, name, user=None, read=1, write=0, share=0, everyone=0, flags=No }) doc.save(ignore_permissions=True) + notify_assignment(user, doctype, name, description=None, notify=notify) return doc @@ -134,3 +136,20 @@ def check_share_permission(doctype, name): """Check if the user can share with other users""" if not frappe.has_permission(doctype, ptype="share", doc=name): frappe.throw(_("No permission to {0} {1} {2}".format("share", doctype, name)), frappe.PermissionError) + +def notify_assignment(shared_by, doc_type, doc_name, description=None, notify=0): + + if not (shared_by and doc_type and doc_name): return + + from frappe.utils import get_link_to_form + document = get_link_to_form(doc_type, doc_name, label="%s: %s" % (doc_type, doc_name)) + + arg = { + 'contact': shared_by, + 'txt': _("A new document {0} has been shared by with you {1}.").format(document, + shared_by), + 'notify': notify + } + + from frappe.desk.page.chat import chat + chat.post(**arg) From 144f5e23bdd07c380ac20d759a05b475de04cb10 Mon Sep 17 00:00:00 2001 From: shridhar Date: Fri, 15 Sep 2017 16:35:55 +0530 Subject: [PATCH 019/135] Email notification on docshare Send an email notification when a doc is shared New notify_by_email colum added in docshare Checkbox added in set_sharing.html --- frappe/core/doctype/docshare/docshare.json | 513 +++++++++--------- frappe/public/js/frappe/form/share.js | 3 +- .../js/frappe/form/templates/set_sharing.html | 2 + frappe/share.py | 21 +- 4 files changed, 295 insertions(+), 244 deletions(-) diff --git a/frappe/core/doctype/docshare/docshare.json b/frappe/core/doctype/docshare/docshare.json index 83d837b7f8..a4efb6bd4d 100644 --- a/frappe/core/doctype/docshare/docshare.json +++ b/frappe/core/doctype/docshare/docshare.json @@ -1,264 +1,293 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "hash", - "beta": 0, - "creation": "2015-02-04 04:33:36.330477", - "custom": 0, - "description": "Internal record of document shares", - "docstatus": 0, - "doctype": "DocType", - "document_type": "System", - "editable_grid": 0, + "allow_copy": 0, + "allow_import": 1, + "allow_rename": 0, + "autoname": "hash", + "beta": 0, + "creation": "2015-02-04 04:33:36.330477", + "custom": 0, + "description": "Internal record of document shares", + "docstatus": 0, + "doctype": "DocType", + "document_type": "System", + "editable_grid": 0, "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "user", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "User", - "length": 0, - "no_copy": 0, - "options": "User", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "user", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "User", + "length": 0, + "no_copy": 0, + "options": "User", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 1, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "share_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": "Document Type", - "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": 1, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "share_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": "Document Type", + "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": 1, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "share_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": "Document Name", - "length": 0, - "no_copy": 0, - "options": "share_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": 1, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "share_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": "Document Name", + "length": 0, + "no_copy": 0, + "options": "share_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": 1, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fieldname": "read", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Read", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "0", + "fieldname": "read", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Read", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fieldname": "write", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Write", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "0", + "fieldname": "write", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Write", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fieldname": "share", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Share", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "0", + "fieldname": "share", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Share", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "everyone", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Everyone", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "everyone", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Everyone", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "1", + "fieldname": "notify_by_email", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Notify by email", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 1, - "in_dialog": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2016-12-29 14:40:40.284335", - "modified_by": "Administrator", - "module": "Core", - "name": "DocShare", - "name_case": "", - "owner": "Administrator", + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 1, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-09-15 15:58:34.126438", + "modified_by": "Administrator", + "module": "Core", + "name": "DocShare", + "name_case": "", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 0, - "export": 1, - "if_owner": 0, - "import": 1, - "is_custom": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 0, + "export": 1, + "if_owner": 0, + "import": 1, + "permlevel": 0, + "print": 0, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, "write": 1 } - ], - "quick_entry": 0, - "read_only": 1, - "read_only_onload": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, + ], + "quick_entry": 0, + "read_only": 1, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, "track_seen": 0 -} \ No newline at end of file +} diff --git a/frappe/public/js/frappe/form/share.js b/frappe/public/js/frappe/form/share.js index 828ea69ede..a44a61eeb4 100644 --- a/frappe/public/js/frappe/form/share.js +++ b/frappe/public/js/frappe/form/share.js @@ -140,7 +140,8 @@ frappe.ui.form.Share = Class.extend({ user: user, read: $(d.body).find(".add-share-read").prop("checked") ? 1 : 0, write: $(d.body).find(".add-share-write").prop("checked") ? 1 : 0, - share: $(d.body).find(".add-share-share").prop("checked") ? 1 : 0 + share: $(d.body).find(".add-share-share").prop("checked") ? 1 : 0, + notify: $(d.body).find(".add-share-notify").prop("checked") ? 1 : 0 }, btn: this, callback: function(r) { diff --git a/frappe/public/js/frappe/form/templates/set_sharing.html b/frappe/public/js/frappe/form/templates/set_sharing.html index 0452dc7fd6..33e19c0139 100644 --- a/frappe/public/js/frappe/form/templates/set_sharing.html +++ b/frappe/public/js/frappe/form/templates/set_sharing.html @@ -51,5 +51,7 @@

+ {%= __("Notify via email")%} + {% } %} diff --git a/frappe/share.py b/frappe/share.py index e277570504..3ca0d13f12 100644 --- a/frappe/share.py +++ b/frappe/share.py @@ -7,8 +7,9 @@ from frappe import _ from frappe.utils import cint @frappe.whitelist() -def add(doctype, name, user=None, read=1, write=0, share=0, everyone=0, flags=None): +def add(doctype, name, user=None, read=1, write=0, share=0, everyone=0, flags=None, notify=0): """Share the given document with a user.""" + print notify, share if not user: user = frappe.session.user @@ -39,6 +40,7 @@ def add(doctype, name, user=None, read=1, write=0, share=0, everyone=0, flags=No }) doc.save(ignore_permissions=True) + notify_assignment(user, doctype, name, description=None, notify=notify) return doc @@ -134,3 +136,20 @@ def check_share_permission(doctype, name): """Check if the user can share with other users""" if not frappe.has_permission(doctype, ptype="share", doc=name): frappe.throw(_("No permission to {0} {1} {2}".format("share", doctype, name)), frappe.PermissionError) + +def notify_assignment(shared_by, doc_type, doc_name, description=None, notify=0): + + if not (shared_by and doc_type and doc_name): return + + from frappe.utils import get_link_to_form + document = get_link_to_form(doc_type, doc_name, label="%s: %s" % (doc_type, doc_name)) + + arg = { + 'contact': shared_by, + 'txt': _("A new document {0} has been shared by with you {1}.").format(document, + shared_by), + 'notify': notify + } + + from frappe.desk.page.chat import chat + chat.post(**arg) From 421b43680311ba004443839f7707001361f510dd Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Mon, 18 Sep 2017 16:32:33 +0530 Subject: [PATCH 020/135] move run webhook to Document from hooks --- frappe/hooks.py | 17 ++----- .../integrations/doctype/webhook/webhook.py | 23 +++++++++ frappe/model/document.py | 47 +++++++++++++++++++ 3 files changed, 73 insertions(+), 14 deletions(-) diff --git a/frappe/hooks.py b/frappe/hooks.py index 9fdca1a87a..f5d98f0421 100755 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -108,25 +108,14 @@ doc_events = { "*": { "on_update": [ "frappe.desk.notifications.clear_doctype_notifications", - "frappe.core.doctype.communication.feed.update_feed", - "frappe.integrations.webhooks.doc_event_webhook" + "frappe.core.doctype.communication.feed.update_feed" ], "after_rename": "frappe.desk.notifications.clear_doctype_notifications", "on_cancel": [ "frappe.desk.notifications.clear_doctype_notifications", - "frappe.integrations.webhooks.doc_event_webhook" ], - "on_trash": [ - "frappe.desk.notifications.clear_doctype_notifications", - "frappe.integrations.webhooks.doc_event_webhook" - ], - "on_change": [ - "frappe.core.doctype.feedback_trigger.feedback_trigger.trigger_feedback_request", - "frappe.integrations.webhooks.doc_event_webhook" - ], - "after_insert": "frappe.integrations.webhooks.doc_event_webhook", - "on_submit": "frappe.integrations.webhooks.doc_event_webhook", - "on_update_after_submit": "frappe.integrations.webhooks.doc_event_webhook" + "on_trash": "frappe.desk.notifications.clear_doctype_notifications", + "on_change": "frappe.core.doctype.feedback_trigger.feedback_trigger.trigger_feedback_request" }, "Email Group Member": { "validate": "frappe.email.doctype.email_group.email_group.restrict_email_group" diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index 5f74b22ac4..10b07adcba 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe +import json, requests from frappe import _ from frappe.model.document import Document from six.moves.urllib.parse import urlparse @@ -35,3 +36,25 @@ class Webhook(Document): if len(webhook_data)!= len(set(webhook_data)): frappe.throw(_("Same Field is entered more than once")) + def request(self, doc): + headers = {} + data = {} + if self.webhook_headers: + for h in self.webhook_headers: + if h.get("key") and h.get("value"): + headers[h.get("key")] = h.get("value") + if self.webhook_data: + for k, v in doc.as_dict().items(): + for w in self.webhook_data: + if k == w.fieldname: + data[w.key] = v + r = requests.post(self.request_url, data=json.dumps(data), headers=headers, timeout=5) + frappe.logger().debug({"webhook_success":r.text}) + +def evaluate_webhook(doc, webhook): + webhook = frappe.get_doc("Webhook", webhook.get("name")) + try: + webhook.request(doc) + except Exception as e: + frappe.log_error(message=frappe.get_traceback(), title=e) + frappe.throw(_("Error in Webhook")) diff --git a/frappe/model/document.py b/frappe/model/document.py index 033c807418..d43f487e8f 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -169,6 +169,7 @@ class Document(BaseDocument): return self.flags.email_alerts_executed = [] + self.flags.webhooks_executed = [] if ignore_permissions!=None: self.flags.ignore_permissions = ignore_permissions @@ -242,6 +243,7 @@ class Document(BaseDocument): return self.flags.email_alerts_executed = [] + self.flags.webhooks_executed = [] if ignore_permissions!=None: self.flags.ignore_permissions = ignore_permissions @@ -671,6 +673,7 @@ class Document(BaseDocument): out = Document.hook(fn)(self, *args, **kwargs) self.run_email_alerts(method) + self.run_webhooks(method) return out @@ -723,6 +726,50 @@ class Document(BaseDocument): elif alert.event=='Method' and method == alert.method: _evaluate_alert(alert) + def run_webhooks(self, method): + '''Run webhooks for this method''' + if frappe.flags.in_import or frappe.flags.in_patch or frappe.flags.in_install: + return + + if self.flags.webhooks_executed==None: + self.flags.webhooks_executed = [] + + from frappe.integrations.doctype.webhook.webhook import evaluate_webhook + + if self.flags.webhooks == None: + webhooks = frappe.cache().hget('webhooks', self.doctype) + if webhooks==None: + webhooks = frappe.get_all('Webhook', + fields=["name", "webhook_docevent", "webhook_doctype"]) + frappe.cache().hset('webhooks', self.doctype, webhooks) + self.flags.webhooks = webhooks + + if not self.flags.webhooks: + return + + def _evaluate_webhook(webhook): + if not webhook.name in self.flags.webhooks_executed: + evaluate_webhook(self, webhook) + self.flags.webhooks_executed.append(webhook.name) + + event_map = { + "on_update": "on_update", + "after_insert": "after_insert", + "on_submit": "on_submit", + "on_cancel": "on_cancel", + "on_trash": "on_trash" + } + + if not self.flags.in_insert: + # value change is not applicable in insert + event_map['on_change'] = 'on_change' + event_map['before_update_after_submit'] = 'before_update_after_submit' + + for webhook in self.flags.webhooks: + event = event_map.get(method, None) + if event and webhook.webhook_docevent == event: + _evaluate_webhook(webhook) + @staticmethod def whitelist(f): f.whitelisted = True From fadb45e369dcdb9697fb63fdd8a6c7d77a8272bc Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Mon, 18 Sep 2017 17:10:53 +0530 Subject: [PATCH 021/135] enqueue webhook request --- .../integrations/doctype/webhook/webhook.py | 8 +++-- frappe/integrations/webhooks.py | 33 ------------------- 2 files changed, 5 insertions(+), 36 deletions(-) delete mode 100644 frappe/integrations/webhooks.py diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index 10b07adcba..1fe9161f51 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -51,10 +51,12 @@ class Webhook(Document): r = requests.post(self.request_url, data=json.dumps(data), headers=headers, timeout=5) frappe.logger().debug({"webhook_success":r.text}) -def evaluate_webhook(doc, webhook): +def enqueue_webhook(doc, webhook): webhook = frappe.get_doc("Webhook", webhook.get("name")) try: webhook.request(doc) except Exception as e: - frappe.log_error(message=frappe.get_traceback(), title=e) - frappe.throw(_("Error in Webhook")) + frappe.throw(_("Error in Webhook"), exc=e) + +def evaluate_webhook(doc, webhook): + frappe.enqueue('frappe.integrations.doctype.webhook.webhook.enqueue_webhook', doc=doc, webhook=webhook) diff --git a/frappe/integrations/webhooks.py b/frappe/integrations/webhooks.py deleted file mode 100644 index 466a0f0470..0000000000 --- a/frappe/integrations/webhooks.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -from __future__ import unicode_literals -import frappe, requests, json -from frappe import _ - -# Doc Events Webhook -def doc_event_webhook(doc, method=None, *args, **kwargs): - headers = {} - data = {} - filters = { - "webhook_doctype": doc.get("doctype"), - "webhook_docevent": method - } - webhooks = frappe.get_all("Webhook", filters=filters) - webhook = frappe.get_doc("Webhook", webhooks[0].get("name")) if webhooks and len(webhooks) > 0 else None - if webhook: - if webhook.webhook_headers: - for h in webhook.webhook_headers: - if h.get("key") and h.get("value"): - headers[h.get("key")] = h.get("value") - if webhook.webhook_data: - for k, v in doc.as_dict().items(): - for w in webhook.webhook_data: - if k == w.fieldname: - data[w.key] = v - try: - r = requests.post(webhook.request_url, data=json.dumps(data), headers=headers, timeout=5) - frappe.logger().debug({"webhook_success":r.text, "webhook": webhook.as_json()}) - except Exception as e: - frappe.logger().debug({"webhook_error":r.text, "webhook": webhook.as_json()}) - frappe.throw(_("Unable to make request"), exc=e) From 92973a1cf9f5967fdc723f5a3aaace66f4a3e9b3 Mon Sep 17 00:00:00 2001 From: mbauskar Date: Fri, 15 Sep 2017 12:47:40 +0530 Subject: [PATCH 022/135] [minor] don't setup checkbox if the childtable is read_only --- frappe/public/js/frappe/form/grid.js | 3 ++- frappe/public/js/frappe/form/grid_row.js | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index f1a5ab10fe..0a3b2db48c 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -142,6 +142,8 @@ frappe.ui.form.Grid = Class.extend({ grid: this }); } + // to hide checkbox if grid is not editable + this.header_row.toggle_check(); }, refresh: function(force) { !this.wrapper && this.make(); @@ -170,7 +172,6 @@ frappe.ui.form.Grid = Class.extend({ } else { // redraw var _scroll_y = $(document).scrollTop(); - this.make_head(); if(!this.grid_rows) { diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index aee170b35c..cea9c04551 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -2,10 +2,10 @@ frappe.ui.form.GridRow = Class.extend({ init: function(opts) { this.on_grid_fields_dict = {}; this.on_grid_fields = []; - this.row_check_html = ''; this.columns = {}; this.columns_list = []; $.extend(this, opts); + this.row_check_html = ''; this.make(); }, make: function() { @@ -121,6 +121,8 @@ frappe.ui.form.GridRow = Class.extend({ if(this.grid_form) { this.grid_form.layout && this.grid_form.layout.refresh(this.doc); } + + this.toggle_check(); }, render_template: function() { this.set_row_index(); @@ -592,4 +594,10 @@ frappe.ui.form.GridRow = Class.extend({ toggle_editable: function(fieldname, editable) { this.set_field_property(fieldname, 'read_only', editable ? 0 : 1); }, + toggle_check: function() { + // to hide checkbox if grid is not editable + this.wrapper + .find('.grid-row-check') + .css("display", this.grid.is_editable()? 'block':'none'); + } }); \ No newline at end of file From 7ddc6ce933124f89be8d87f4ebbe33aaea5c82f6 Mon Sep 17 00:00:00 2001 From: Manas Solanki Date: Tue, 19 Sep 2017 14:07:01 +0530 Subject: [PATCH 023/135] try catch expression in eval for depends_on --- frappe/public/js/frappe/form/layout.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 9ec3ee38a3..fc5e70a345 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -441,7 +441,12 @@ frappe.ui.form.Layout = Class.extend({ var parent = this.frm ? this.frm.doc : null; if(expression.substr(0,5)=='eval:') { - out = eval(expression.substr(5)); + try { + out = eval(expression.substr(5)); + } catch(e) { + frappe.throw(_('Invalid "depends_on" expression')) + } + } else if(expression.substr(0,3)=='fn:' && this.frm) { out = this.frm.script_manager.trigger(expression.substr(3), this.doctype, this.docname); } else { From e7459c7e83ad5c5c68b2c458bcac4474194f217f Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Tue, 19 Sep 2017 16:40:17 +0530 Subject: [PATCH 024/135] Clean up --- .../integrations/doctype/webhook/webhook.py | 2 +- frappe/model/document.py | 24 +++++++------------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index 1fe9161f51..8bbcbdc5ad 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -58,5 +58,5 @@ def enqueue_webhook(doc, webhook): except Exception as e: frappe.throw(_("Error in Webhook"), exc=e) -def evaluate_webhook(doc, webhook): +def webhook_request(doc, webhook): frappe.enqueue('frappe.integrations.doctype.webhook.webhook.enqueue_webhook', doc=doc, webhook=webhook) diff --git a/frappe/model/document.py b/frappe/model/document.py index d43f487e8f..2e83f085f3 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -734,41 +734,35 @@ class Document(BaseDocument): if self.flags.webhooks_executed==None: self.flags.webhooks_executed = [] - from frappe.integrations.doctype.webhook.webhook import evaluate_webhook + from frappe.integrations.doctype.webhook.webhook import webhook_request if self.flags.webhooks == None: webhooks = frappe.cache().hget('webhooks', self.doctype) if webhooks==None: webhooks = frappe.get_all('Webhook', - fields=["name", "webhook_docevent", "webhook_doctype"]) + fields=["name", "webhook_docevent"]) frappe.cache().hset('webhooks', self.doctype, webhooks) self.flags.webhooks = webhooks if not self.flags.webhooks: return - def _evaluate_webhook(webhook): + def _webhook_request(webhook): if not webhook.name in self.flags.webhooks_executed: - evaluate_webhook(self, webhook) + webhook_request(self, webhook) self.flags.webhooks_executed.append(webhook.name) - event_map = { - "on_update": "on_update", - "after_insert": "after_insert", - "on_submit": "on_submit", - "on_cancel": "on_cancel", - "on_trash": "on_trash" - } + event_list = ["on_update", "after_insert", "on_submit", "on_cancel", "on_trash"] if not self.flags.in_insert: # value change is not applicable in insert - event_map['on_change'] = 'on_change' - event_map['before_update_after_submit'] = 'before_update_after_submit' + event_list.append('on_change') + event_list.append('before_update_after_submit') for webhook in self.flags.webhooks: - event = event_map.get(method, None) + event = method if method in event_list else None if event and webhook.webhook_docevent == event: - _evaluate_webhook(webhook) + _webhook_request(webhook) @staticmethod def whitelist(f): From 9b1f46d2c86ea952dbde6062f8828d713080bce0 Mon Sep 17 00:00:00 2001 From: mbauskar Date: Wed, 20 Sep 2017 12:02:22 +0530 Subject: [PATCH 025/135] [minor] move the toggle_check funcation call to refresh --- frappe/public/js/frappe/form/grid.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 0a3b2db48c..ad018a1c79 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -142,8 +142,6 @@ frappe.ui.form.Grid = Class.extend({ grid: this }); } - // to hide checkbox if grid is not editable - this.header_row.toggle_check(); }, refresh: function(force) { !this.wrapper && this.make(); @@ -173,6 +171,8 @@ frappe.ui.form.Grid = Class.extend({ // redraw var _scroll_y = $(document).scrollTop(); this.make_head(); + // to hide checkbox if grid is not editable + this.header_row && this.header_row.toggle_check(); if(!this.grid_rows) { this.grid_rows = []; From 1f9b1e40e36134210acb71213d220eb0dfad38c7 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Wed, 20 Sep 2017 16:33:28 +0530 Subject: [PATCH 026/135] Moved run_webhooks to webhook.py --- .../integrations/doctype/webhook/webhook.py | 38 +++++++++++++++- frappe/model/document.py | 45 ++----------------- 2 files changed, 40 insertions(+), 43 deletions(-) diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index 8bbcbdc5ad..6a95085c24 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -49,6 +49,7 @@ class Webhook(Document): if k == w.fieldname: data[w.key] = v r = requests.post(self.request_url, data=json.dumps(data), headers=headers, timeout=5) + r.raise_for_status() frappe.logger().debug({"webhook_success":r.text}) def enqueue_webhook(doc, webhook): @@ -58,5 +59,38 @@ def enqueue_webhook(doc, webhook): except Exception as e: frappe.throw(_("Error in Webhook"), exc=e) -def webhook_request(doc, webhook): - frappe.enqueue('frappe.integrations.doctype.webhook.webhook.enqueue_webhook', doc=doc, webhook=webhook) +def run_webhooks(doc, method): + '''Run webhooks for this method''' + if frappe.flags.in_import or frappe.flags.in_patch or frappe.flags.in_install: + return + + if not getattr(frappe.local, 'webhooks_executed', None): + frappe.local.webhooks_executed = [] + + if doc.flags.webhooks == None: + webhooks = frappe.cache().hget('webhooks', doc.doctype) + if webhooks==None: + webhooks = frappe.get_all('Webhook', + fields=["name", "webhook_docevent"]) + frappe.cache().hset('webhooks', doc.doctype, webhooks) + doc.flags.webhooks = webhooks + + if not doc.flags.webhooks: + return + + def _webhook_request(webhook): + if not webhook.name in frappe.local.webhooks_executed: + frappe.enqueue("frappe.integrations.doctype.webhook.webhook.enqueue_webhook", doc=doc, webhook=webhook) + frappe.local.webhooks_executed.append(webhook.name) + + event_list = ["on_update", "after_insert", "on_submit", "on_cancel", "on_trash"] + + if not doc.flags.in_insert: + # value change is not applicable in insert + event_list.append('on_change') + event_list.append('before_update_after_submit') + + for webhook in doc.flags.webhooks: + event = method if method in event_list else None + if event and webhook.webhook_docevent == event: + _webhook_request(webhook) diff --git a/frappe/model/document.py b/frappe/model/document.py index 2e83f085f3..5e4bfc01c4 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -169,7 +169,7 @@ class Document(BaseDocument): return self.flags.email_alerts_executed = [] - self.flags.webhooks_executed = [] + setattr(frappe.local, 'webhooks_executed', []) if ignore_permissions!=None: self.flags.ignore_permissions = ignore_permissions @@ -243,7 +243,7 @@ class Document(BaseDocument): return self.flags.email_alerts_executed = [] - self.flags.webhooks_executed = [] + setattr(frappe.local, 'webhooks_executed', []) if ignore_permissions!=None: self.flags.ignore_permissions = ignore_permissions @@ -673,7 +673,8 @@ class Document(BaseDocument): out = Document.hook(fn)(self, *args, **kwargs) self.run_email_alerts(method) - self.run_webhooks(method) + from frappe.integrations.doctype.webhook.webhook import run_webhooks + run_webhooks(self, method) return out @@ -726,44 +727,6 @@ class Document(BaseDocument): elif alert.event=='Method' and method == alert.method: _evaluate_alert(alert) - def run_webhooks(self, method): - '''Run webhooks for this method''' - if frappe.flags.in_import or frappe.flags.in_patch or frappe.flags.in_install: - return - - if self.flags.webhooks_executed==None: - self.flags.webhooks_executed = [] - - from frappe.integrations.doctype.webhook.webhook import webhook_request - - if self.flags.webhooks == None: - webhooks = frappe.cache().hget('webhooks', self.doctype) - if webhooks==None: - webhooks = frappe.get_all('Webhook', - fields=["name", "webhook_docevent"]) - frappe.cache().hset('webhooks', self.doctype, webhooks) - self.flags.webhooks = webhooks - - if not self.flags.webhooks: - return - - def _webhook_request(webhook): - if not webhook.name in self.flags.webhooks_executed: - webhook_request(self, webhook) - self.flags.webhooks_executed.append(webhook.name) - - event_list = ["on_update", "after_insert", "on_submit", "on_cancel", "on_trash"] - - if not self.flags.in_insert: - # value change is not applicable in insert - event_list.append('on_change') - event_list.append('before_update_after_submit') - - for webhook in self.flags.webhooks: - event = method if method in event_list else None - if event and webhook.webhook_docevent == event: - _webhook_request(webhook) - @staticmethod def whitelist(f): f.whitelisted = True From 2d148c804e65d4e5405dde1666801c83023cb227 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Wed, 20 Sep 2017 18:34:28 +0530 Subject: [PATCH 027/135] Webhook retry 3 times --- .../integrations/doctype/webhook/webhook.py | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index 6a95085c24..29b6bde639 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -8,6 +8,7 @@ import json, requests from frappe import _ from frappe.model.document import Document from six.moves.urllib.parse import urlparse +from time import sleep class Webhook(Document): def autoname(self): @@ -36,28 +37,34 @@ class Webhook(Document): if len(webhook_data)!= len(set(webhook_data)): frappe.throw(_("Same Field is entered more than once")) - def request(self, doc): - headers = {} - data = {} - if self.webhook_headers: - for h in self.webhook_headers: - if h.get("key") and h.get("value"): - headers[h.get("key")] = h.get("value") - if self.webhook_data: - for k, v in doc.as_dict().items(): - for w in self.webhook_data: - if k == w.fieldname: - data[w.key] = v - r = requests.post(self.request_url, data=json.dumps(data), headers=headers, timeout=5) - r.raise_for_status() - frappe.logger().debug({"webhook_success":r.text}) def enqueue_webhook(doc, webhook): webhook = frappe.get_doc("Webhook", webhook.get("name")) - try: - webhook.request(doc) - except Exception as e: - frappe.throw(_("Error in Webhook"), exc=e) + headers = {} + data = {} + if webhook.webhook_headers: + for h in webhook.webhook_headers: + if h.get("key") and h.get("value"): + headers[h.get("key")] = h.get("value") + if webhook.webhook_data: + for w in webhook.webhook_data: + for k, v in doc.as_dict().items(): + if k == w.fieldname: + data[w.key] = v + error_status = [] + for i in range(3): + try: + r = requests.post(webhook.request_url, data=json.dumps(data), headers=headers, timeout=5) + r.raise_for_status() + frappe.logger().debug({"webhook_success":r.text}) + break + except Exception as e: + frappe.logger().debug({"webhook_error":e, "try": i+1}) + sleep(3*i + 1) + if i !=2: + continue + else: + raise e def run_webhooks(doc, method): '''Run webhooks for this method''' @@ -71,7 +78,7 @@ def run_webhooks(doc, method): webhooks = frappe.cache().hget('webhooks', doc.doctype) if webhooks==None: webhooks = frappe.get_all('Webhook', - fields=["name", "webhook_docevent"]) + fields=["name", "webhook_docevent", "webhook_doctype"]) frappe.cache().hset('webhooks', doc.doctype, webhooks) doc.flags.webhooks = webhooks @@ -92,5 +99,5 @@ def run_webhooks(doc, method): for webhook in doc.flags.webhooks: event = method if method in event_list else None - if event and webhook.webhook_docevent == event: + if event and webhook.webhook_docevent == event and webhook.webhook_doctype == doc.doctype: _webhook_request(webhook) From 45f2ff43addfc374034f2b911ba29f998b37a122 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Wed, 20 Sep 2017 18:52:50 +0530 Subject: [PATCH 028/135] [Fix] Codacy https://www.codacy.com/app/frappe/frappe/pullRequest?prid=900990 --- frappe/integrations/doctype/webhook/webhook.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index 29b6bde639..26ecc36413 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -51,7 +51,6 @@ def enqueue_webhook(doc, webhook): for k, v in doc.as_dict().items(): if k == w.fieldname: data[w.key] = v - error_status = [] for i in range(3): try: r = requests.post(webhook.request_url, data=json.dumps(data), headers=headers, timeout=5) From 486ea5bb05b1281f16d7c8c42d4f905b2ac1b37a Mon Sep 17 00:00:00 2001 From: Manas Solanki Date: Thu, 21 Sep 2017 12:24:18 +0530 Subject: [PATCH 029/135] fix codacy --- frappe/public/js/frappe/form/layout.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index fc5e70a345..2334ab9876 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -444,7 +444,7 @@ frappe.ui.form.Layout = Class.extend({ try { out = eval(expression.substr(5)); } catch(e) { - frappe.throw(_('Invalid "depends_on" expression')) + frappe.throw(__('Invalid "depends_on" expression')); } } else if(expression.substr(0,3)=='fn:' && this.frm) { From 82c9357b50165752759c6297ef4eaacb9fd4f48c Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 21 Sep 2017 14:54:58 +0530 Subject: [PATCH 030/135] [fix] item selector --- frappe/public/css/list.css | 16 +++++++++++++++ frappe/public/js/frappe/assets.js | 2 +- frappe/public/js/frappe/form/grid.js | 2 +- .../js/frappe/form/templates/grid_body.html | 4 +++- .../js/frappe/ui/toolbar/search_utils.js | 2 +- frappe/public/less/list.less | 20 +++++++++++++++++++ 6 files changed, 42 insertions(+), 4 deletions(-) diff --git a/frappe/public/css/list.css b/frappe/public/css/list.css index 49ecce16de..abed4bc105 100644 --- a/frappe/public/css/list.css +++ b/frappe/public/css/list.css @@ -288,6 +288,10 @@ display: flex; flex-wrap: wrap; } +.image-view-container .image-view-row { + display: flex; + border-bottom: 1px solid #ebeff2; +} .image-view-container .image-view-item { flex: 0 0 25%; padding: 15px; @@ -360,6 +364,18 @@ border-bottom: 1px solid #EBEFF2; } } +.item-selector { + border: 1px solid #d1d8dd; +} +.item-selector .image-view-row { + width: 100%; +} +.item-selector .image-field { + height: 120px; +} +.item-selector .placeholder-text { + font-size: 48px; +} .image-view-container.three-column .image-view-item { flex: 0 0 33.33333333%; } diff --git a/frappe/public/js/frappe/assets.js b/frappe/public/js/frappe/assets.js index 8cdbfeafe0..71c576e291 100644 --- a/frappe/public/js/frappe/assets.js +++ b/frappe/public/js/frappe/assets.js @@ -85,7 +85,7 @@ frappe.assets = { frappe.assets.executed_.push(path) } } - callback(); + callback && callback(); }, // check if the asset exists in diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index ad018a1c79..93fdabdb27 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -653,7 +653,7 @@ frappe.ui.form.Grid = Class.extend({ var btn = this.custom_buttons[label]; if(!btn) { btn = $('') - .css('margin-right', '10px') + .css('margin-right', '4px') .prependTo(this.grid_buttons) .on('click', click); this.custom_buttons[label] = btn; diff --git a/frappe/public/js/frappe/form/templates/grid_body.html b/frappe/public/js/frappe/form/templates/grid_body.html index 8b3a1b6082..5fef1379b9 100644 --- a/frappe/public/js/frappe/form/templates/grid_body.html +++ b/frappe/public/js/frappe/form/templates/grid_body.html @@ -7,7 +7,9 @@
diff --git a/frappe/templates/includes/list/list.html b/frappe/templates/includes/list/list.html index bf2efba02b..70e7cb25d1 100644 --- a/frappe/templates/includes/list/list.html +++ b/frappe/templates/includes/list/list.html @@ -1,5 +1,5 @@ {% if sub_title %} -

{{ sub_title }}

+

{{ sub_title }}

{% endif %} {% if not result -%}
diff --git a/frappe/website/doctype/blog_post/blog_post.py b/frappe/website/doctype/blog_post/blog_post.py index 29e0aef8e9..3c28c47cf6 100644 --- a/frappe/website/doctype/blog_post/blog_post.py +++ b/frappe/website/doctype/blog_post/blog_post.py @@ -190,7 +190,7 @@ def get_blog_list(doctype, txt=None, filters=None, limit_start=0, limit_page_len post.category = frappe.db.get_value('Blog Category', post.blog_category, ['route', 'title'], as_dict=True) - if (not "http:" in post.avatar or "https:" in post.avatar) and not post.avatar.startswith("/"): + if post.avatar and (not "http:" in post.avatar or "https:" in post.avatar) and not post.avatar.startswith("/"): post.avatar = "/" + post.avatar return posts diff --git a/frappe/website/doctype/blog_post/templates/blog_post.html b/frappe/website/doctype/blog_post/templates/blog_post.html index 24a128ddd6..d1682593a0 100644 --- a/frappe/website/doctype/blog_post/templates/blog_post.html +++ b/frappe/website/doctype/blog_post/templates/blog_post.html @@ -11,13 +11,12 @@ border-top: 1px solid {{ style.border_color }}; } .blog-info { - text-align:center; margin-top: 30px; } .blog-text { padding-top: 50px; padding-bottom: 50px; - font-size: 18px; + font-size: 16px; line-height: 1.5; } .blog-text p { @@ -28,13 +27,13 @@ padding-bottom: 50px; } .blogger-name { - margin-top: 0px; + margin-top: 5px; }
- - + +

{{ title }}

diff --git a/frappe/website/doctype/blog_post/templates/blog_post_row.html b/frappe/website/doctype/blog_post/templates/blog_post_row.html index 42c1833920..1ea67812f1 100644 --- a/frappe/website/doctype/blog_post/templates/blog_post_row.html +++ b/frappe/website/doctype/blog_post/templates/blog_post_row.html @@ -2,22 +2,34 @@
-

{{ post.title }}

+

{{ post.title }}

{% if post.cover_image %}

{{post.title}} - Cover Image

{% endif %}

{{ post.intro }}

-

- {{ _("By") }} {{ post.full_name }} - {{ frappe.format_date(post.published_on) }} - - {{ post.category.title }} - {{ post.comment_text }} -

+ +
+
+
+ +
+
+
+ +
+ {{ frappe.format_date(post.published_on) }} + {% if post.comments %} + + {{ post.comments }} + + {% endif %} +
+
+
From 0df3335e9e982e9baf985877cf0fa3ca2f456ce8 Mon Sep 17 00:00:00 2001 From: creador30 Date: Sun, 24 Sep 2017 11:12:52 +0300 Subject: [PATCH 050/135] [fix] dropbox upload of unicode filenames When using Unicode filenames Dropbox backup fails with an error similar to: UnicodeEncodeError: 'ascii' codec can't encode character u'\u015f' in position 44: ordinal not in range(128) This commit fixes this issue. --- .../integrations/doctype/dropbox_settings/dropbox_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py index 3ceae47a0e..a839049d7b 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py @@ -146,7 +146,7 @@ def upload_from_folder(path, dropbox_folder, dropbox_client, did_not_upload, err def upload_file_to_dropbox(filename, folder, dropbox_client): create_folder_if_not_exists(folder, dropbox_client) chunk_size = 4 * 1024 * 1024 - file_size = os.path.getsize(filename) + file_size = os.path.getsize(encode(filename)) mode = (dropbox.files.WriteMode.overwrite) f = open(encode(filename), 'rb') From a6056ea510358c41447a49df6eff973f0044f2f6 Mon Sep 17 00:00:00 2001 From: Achilles Rasquinha Date: Sun, 24 Sep 2017 17:23:35 +0530 Subject: [PATCH 051/135] added .vscode to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 75bee9a091..4dd4f3f6ce 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ locale dist/ build/ frappe/docs/current +.vscode From 18d17e2fd585dbf709ab89ecf57f08a0ad5e1a1c Mon Sep 17 00:00:00 2001 From: Manas Solanki Date: Sun, 24 Sep 2017 19:11:24 +0530 Subject: [PATCH 052/135] datepicker in the web form --- frappe/website/js/web_form.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frappe/website/js/web_form.js b/frappe/website/js/web_form.js index fdef813acc..d5bcabc649 100644 --- a/frappe/website/js/web_form.js +++ b/frappe/website/js/web_form.js @@ -373,7 +373,10 @@ frappe.ready(function() { $dates.datepicker({ language: "en", autoClose: true, - dateFormat: frappe.datepicker_format + dateFormat: frappe.datepicker_format, + onSelect: function(date, date_str, e) { + e.$el.trigger('change'); + } }); // initialize dates from YYYY-MM-DD to user format @@ -419,7 +422,7 @@ frappe.ready(function() { }); }); - } + }; setup_text_editor(); }); From a52d927e286a5b259fdc7ff64d7cc55e75a3ad10 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Sun, 24 Sep 2017 20:51:29 +0530 Subject: [PATCH 053/135] [fix] for blog --- frappe/website/doctype/blog_post/blog_post.py | 2 +- frappe/website/doctype/blog_post/templates/blog_post_row.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/website/doctype/blog_post/blog_post.py b/frappe/website/doctype/blog_post/blog_post.py index 3c28c47cf6..b3ea0bd797 100644 --- a/frappe/website/doctype/blog_post/blog_post.py +++ b/frappe/website/doctype/blog_post/blog_post.py @@ -190,7 +190,7 @@ def get_blog_list(doctype, txt=None, filters=None, limit_start=0, limit_page_len post.category = frappe.db.get_value('Blog Category', post.blog_category, ['route', 'title'], as_dict=True) - if post.avatar and (not "http:" in post.avatar or "https:" in post.avatar) and not post.avatar.startswith("/"): + if post.avatar and (not "http:" in post.avatar or not "https:" in post.avatar) and not post.avatar.startswith("/"): post.avatar = "/" + post.avatar return posts diff --git a/frappe/website/doctype/blog_post/templates/blog_post_row.html b/frappe/website/doctype/blog_post/templates/blog_post_row.html index 1ea67812f1..83a47079af 100644 --- a/frappe/website/doctype/blog_post/templates/blog_post_row.html +++ b/frappe/website/doctype/blog_post/templates/blog_post_row.html @@ -5,14 +5,14 @@

{{ post.title }}

{% if post.cover_image %}

- {{post.title}} - Cover Image + {{post.title}} - Cover Image

{% endif %}

{{ post.intro }}

-
+
From 1ee6ff100966f3bff31b92ac209ce87e7b8b7925 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Sun, 24 Sep 2017 20:54:28 +0530 Subject: [PATCH 054/135] [fix] for blog --- frappe/website/doctype/blog_post/blog_post.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/website/doctype/blog_post/blog_post.py b/frappe/website/doctype/blog_post/blog_post.py index b3ea0bd797..a20e9fa128 100644 --- a/frappe/website/doctype/blog_post/blog_post.py +++ b/frappe/website/doctype/blog_post/blog_post.py @@ -190,7 +190,7 @@ def get_blog_list(doctype, txt=None, filters=None, limit_start=0, limit_page_len post.category = frappe.db.get_value('Blog Category', post.blog_category, ['route', 'title'], as_dict=True) - if post.avatar and (not "http:" in post.avatar or not "https:" in post.avatar) and not post.avatar.startswith("/"): + if post.avatar and (not "http:" in post.avatar and not "https:" in post.avatar) and not post.avatar.startswith("/"): post.avatar = "/" + post.avatar return posts From cef8c6be3805aefa141b0a75e7909a439e850a5f Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Sun, 24 Sep 2017 20:59:11 +0530 Subject: [PATCH 055/135] [minor] web_page, allow any tags --- frappe/website/doctype/web_page/web_page.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/website/doctype/web_page/web_page.json b/frappe/website/doctype/web_page/web_page.json index f71eac66d7..f88a059356 100644 --- a/frappe/website/doctype/web_page/web_page.json +++ b/frappe/website/doctype/web_page/web_page.json @@ -262,7 +262,7 @@ "fieldtype": "Text Editor", "hidden": 0, "ignore_user_permissions": 0, - "ignore_xss_filter": 0, + "ignore_xss_filter": 1, "in_filter": 0, "in_global_search": 1, "in_list_view": 0, @@ -834,7 +834,7 @@ "issingle": 0, "istable": 0, "max_attachments": 20, - "modified": "2017-06-29 23:01:00.126453", + "modified": "2017-09-24 20:58:41.588919", "modified_by": "Administrator", "module": "Website", "name": "Web Page", From adc7ad69cdbab3cd5aa2effd3a500a14d87fd54b Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 25 Sep 2017 11:06:54 +0530 Subject: [PATCH 056/135] [fix] desktop notifications --- frappe/core/page/desktop/desktop.js | 4 ++-- .../js/frappe/ui/toolbar/notifications.js | 2 +- .../website/doctype/web_page/test_web_page.js | 23 +++++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 frappe/website/doctype/web_page/test_web_page.js diff --git a/frappe/core/page/desktop/desktop.js b/frappe/core/page/desktop/desktop.js index eae5b7a35d..8b4c062ab6 100644 --- a/frappe/core/page/desktop/desktop.js +++ b/frappe/core/page/desktop/desktop.js @@ -212,8 +212,8 @@ $.extend(frappe.desktop, { notifier.toggle(sum ? true : false); var circle = notifier.find(".circle-text"); var text = sum || ''; - if(text > 20) { - text = '20+'; + if(text > 99) { + text = '99+'; } if(circle.length) { diff --git a/frappe/public/js/frappe/ui/toolbar/notifications.js b/frappe/public/js/frappe/ui/toolbar/notifications.js index 48095c1b8b..dbab78b986 100644 --- a/frappe/public/js/frappe/ui/toolbar/notifications.js +++ b/frappe/public/js/frappe/ui/toolbar/notifications.js @@ -43,7 +43,7 @@ frappe.ui.notifications = { // switch colour on the navbar and disable if no notifications $(".navbar-new-comments") - .html(this.total > 20 ? '20+' : this.total) + .html(this.total > 99 ? '99+' : this.total) .toggleClass("navbar-new-comments-true", this.total ? true : false) .parent().toggleClass("disabled", this.total ? false : true); }, diff --git a/frappe/website/doctype/web_page/test_web_page.js b/frappe/website/doctype/web_page/test_web_page.js new file mode 100644 index 0000000000..0ad6bd58b6 --- /dev/null +++ b/frappe/website/doctype/web_page/test_web_page.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Web Page", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Web Page + () => frappe.tests.make('Web Page', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); From e0410ca7fa4dd9b695293dc96d5c9ba83c1146fb Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 25 Sep 2017 11:21:01 +0530 Subject: [PATCH 057/135] [fix] desktop notifications --- frappe/public/js/frappe/desk.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 02ec07eebf..343c39d5f4 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -260,7 +260,7 @@ frappe.Application = Class.extend({ $.each(frappe.boot.notification_info.open_count_doctype, function(doctype, count) { if(count) { $('.open-notification.global[data-doctype="'+ doctype +'"]') - .removeClass("hide").html(count > 20 ? "20+" : count); + .removeClass("hide").html(count > 99 ? "99+" : count); } else { $('.open-notification.global[data-doctype="'+ doctype +'"]') .addClass("hide"); From 6b517126594a68ea09719a77fb838e89420a9ec9 Mon Sep 17 00:00:00 2001 From: Achilles Rasquinha Date: Mon, 25 Sep 2017 12:38:56 +0530 Subject: [PATCH 058/135] [MINOR, UX] added color contrast to color picker --- frappe/public/js/frappe/form/controls/color.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/color.js b/frappe/public/js/frappe/form/controls/color.js index d13c22d19d..646f7de2f1 100644 --- a/frappe/public/js/frappe/form/controls/color.js +++ b/frappe/public/js/frappe/form/controls/color.js @@ -34,12 +34,15 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({ return `
`; }, set_formatted_input: function(value) { - this._super(value); + this._super(value) + + if (!value) value = '#FFFFFF' + const contrast = frappe.ui.color.get_contrast_color(value) - if(!value) value = '#ffffff'; this.$input.css({ - "background-color": value - }); + "background-color": value, + "color": contrast + }) }, bind_events: function () { var mousedown_happened = false; From ed8b4afde8b6e4d79abb519ffa7268b9f9817eb4 Mon Sep 17 00:00:00 2001 From: Achilles Rasquinha Date: Mon, 25 Sep 2017 12:46:41 +0530 Subject: [PATCH 059/135] fixed codacy --- frappe/public/js/frappe/form/controls/color.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/color.js b/frappe/public/js/frappe/form/controls/color.js index 646f7de2f1..7b14b45aca 100644 --- a/frappe/public/js/frappe/form/controls/color.js +++ b/frappe/public/js/frappe/form/controls/color.js @@ -34,15 +34,15 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({ return `
`; }, set_formatted_input: function(value) { - this._super(value) + this._super(value); - if (!value) value = '#FFFFFF' - const contrast = frappe.ui.color.get_contrast_color(value) + if (!value) value = '#FFFFFF'; + const contrast = frappe.ui.color.get_contrast_color(value); this.$input.css({ "background-color": value, "color": contrast - }) + }); }, bind_events: function () { var mousedown_happened = false; From 147917283a3abf2518aeeb2950e08c1c08970c13 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 25 Sep 2017 12:49:44 +0530 Subject: [PATCH 060/135] [fix] naming only once --- frappe/model/document.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frappe/model/document.py b/frappe/model/document.py index 48e1d18e7d..5b64796c5b 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -334,13 +334,18 @@ class Document(BaseDocument): self._doc_before_save = frappe.get_doc(self.doctype, self.name) return self._doc_before_save - def set_new_name(self): + def set_new_name(self, force=False): """Calls `frappe.naming.se_new_name` for parent and child docs.""" + if self.flags.name_set and not force: + return + set_new_name(self) # set name for children for d in self.get_all_children(): set_new_name(d) + self.flags.name_set = True + def get_title(self): '''Get the document title based on title_field or `title` or `name`''' return self.get(self.meta.get_title_field()) From 97959126a05a67cd0f0ed1f751b782259001ffcb Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 25 Sep 2017 13:11:46 +0530 Subject: [PATCH 061/135] [fix] Set test naming series for test records --- frappe/test_runner.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/test_runner.py b/frappe/test_runner.py index 87d1e1febd..ed826b58d8 100644 --- a/frappe/test_runner.py +++ b/frappe/test_runner.py @@ -294,6 +294,10 @@ def make_test_objects(doctype, test_records=None, verbose=None, reset=False): d = frappe.copy_doc(doc) + if d.meta.get_field("naming_series"): + if not d.naming_series: + d.naming_series = "_T-" + d.doctype + "-" + if doc.get('name'): d.name = doc.get('name') else: @@ -303,10 +307,6 @@ def make_test_objects(doctype, test_records=None, verbose=None, reset=False): # do not create test records, if already exists return [] - if d.meta.get_field("naming_series"): - if not d.naming_series: - d.naming_series = "_T-" + d.doctype + "-" - # submit if docstatus is set to 1 for test record docstatus = d.docstatus From 6626b31360b42bb5626b99dc3645b850020b5188 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 25 Sep 2017 12:13:48 +0530 Subject: [PATCH 062/135] Add to Desktop menu item in tree view fixes frappe/erpnext#10548 --- frappe/public/js/frappe/views/treeview.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/views/treeview.js b/frappe/public/js/frappe/views/treeview.js index a6a8cf5105..bbe176223d 100644 --- a/frappe/public/js/frappe/views/treeview.js +++ b/frappe/public/js/frappe/views/treeview.js @@ -286,7 +286,7 @@ frappe.views.TreeView = Class.extend({ frappe.msgprint(__("You are not allowed to print this report")); return false; } - var tree = $(".tree:visible").html(); + var tree = $(".tree:visible").html(); var me = this; frappe.ui.get_print_settings(false, function(print_settings) { var title = __(me.docname || me.doctype); @@ -339,7 +339,15 @@ frappe.views.TreeView = Class.extend({ if (has_perm) { me.page.add_menu_item(menu_item["label"], menu_item["action"]); } - }) + }); + + // last menu item + me.page.add_menu_item(__('Add to Desktop'), () => { + const label = me.doctype === 'Account' ? + __('Chart of Accounts') : + __(me.doctype); + frappe.add_to_desktop(label, me.doctype); + }); } }); From 8af402bcb644516c6b020e48f122bc886d0ed29b Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 25 Sep 2017 12:50:53 +0530 Subject: [PATCH 063/135] Set correct image url in cordova fixes frappe/erpnext#10630, frappe/mobile#13 --- frappe/public/js/frappe/views/image/image_view.js | 12 ++++++++---- .../js/frappe/views/image/image_view_item_row.html | 8 ++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/frappe/public/js/frappe/views/image/image_view.js b/frappe/public/js/frappe/views/image/image_view.js index 5656c3e78e..6e094909f3 100644 --- a/frappe/public/js/frappe/views/image/image_view.js +++ b/frappe/public/js/frappe/views/image/image_view.js @@ -14,6 +14,12 @@ frappe.views.ImageView = frappe.views.ListRenderer.extend({ this._super(); this.page_title = this.page_title + ' ' + __('Images'); }, + prepare_data: function(data) { + data = this._super(data); + // absolute url if cordova, else relative + data._image_url = this.get_image_url(data); + return data; + }, render_image_view: function () { var html = this.items.map(this.render_item.bind(this)).join(""); this.container = $('
') @@ -22,14 +28,12 @@ frappe.views.ImageView = frappe.views.ListRenderer.extend({ this.container.append(html); }, render_item: function (item) { - var image_url = this.get_image_url(item); var indicator = this.get_indicator_html(item); return frappe.render_template("image_view_item_row", { data: item, indicator: indicator, subject: this.get_subject_html(item, true), additional_columns: this.additional_columns, - item_image: image_url, color: frappe.get_palette(item.item_name) }); }, @@ -112,8 +116,8 @@ frappe.views.GalleryView = Class.extend({ } return { - src: i.image, - msrc: i.image, + src: i._image_url, + msrc: i._image_url, name: i.name, w: width, h: height, diff --git a/frappe/public/js/frappe/views/image/image_view_item_row.html b/frappe/public/js/frappe/views/image/image_view_item_row.html index 80692b8bb8..7eca23dddb 100644 --- a/frappe/public/js/frappe/views/image/image_view_item_row.html +++ b/frappe/public/js/frappe/views/image/image_view_item_row.html @@ -13,18 +13,18 @@
- {% if (!item_image) { %} + {% if (!data._image_url) { %} {%= frappe.get_abbr(data._title) %} {% } %} - {% if (item_image) { %} - {{data.title}} + {% if (data._image_url) { %} + {{data.title}} {% } %}