diff --git a/frappe/__init__.py b/frappe/__init__.py index c5f13f2295..fac0927428 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -348,7 +348,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False, if as_table and type(msg) in (list, tuple): out.as_table = 1 - + if as_list and type(msg) in (list, tuple) and len(msg) > 1: out.as_list = 1 @@ -796,11 +796,17 @@ def get_doc(*args, **kwargs): return doc -def get_last_doc(doctype): +def get_last_doc(doctype, filters=None, order_by="creation desc"): """Get last created document of this type.""" - d = get_all(doctype, ["name"], order_by="creation desc", limit_page_length=1) + d = get_all( + doctype, + filters=filters, + limit_page_length=1, + order_by=order_by, + pluck="name" + ) if d: - return get_doc(doctype, d[0].name) + return get_doc(doctype, d[0]) else: raise DoesNotExistError diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.js b/frappe/automation/doctype/assignment_rule/assignment_rule.js index 774befc15e..ee1a076465 100644 --- a/frappe/automation/doctype/assignment_rule/assignment_rule.js +++ b/frappe/automation/doctype/assignment_rule/assignment_rule.js @@ -57,7 +57,8 @@ frappe.ui.form.on('Assignment Rule', { frm.set_fields_as_options( 'field', doctype, - (df) => df.fieldtype == 'Link' && df.options == 'User', + (df) => ['Dynamic Link', 'Data'].includes(df.fieldtype) + || (df.fieldtype == 'Link' && df.options == 'User'), [{ label: 'Owner', value: 'owner' }] ); if (doctype) { diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.py b/frappe/automation/doctype/assignment_rule/assignment_rule.py index c85cb149ea..d20398d564 100644 --- a/frappe/automation/doctype/assignment_rule/assignment_rule.py +++ b/frappe/automation/doctype/assignment_rule/assignment_rule.py @@ -82,7 +82,7 @@ class AssignmentRule(Document): elif self.rule == 'Load Balancing': return self.get_user_load_balancing() elif self.rule == 'Based on Field': - return doc.get(self.field) + return self.get_user_based_on_field(doc) def get_user_round_robin(self): ''' @@ -119,6 +119,11 @@ class AssignmentRule(Document): # pick the first user return sorted_counts[0].get('user') + def get_user_based_on_field(self, doc): + val = doc.get(self.field) + if frappe.db.exists('User', val): + return val + def safe_eval(self, fieldname, doc): try: if self.get(fieldname): diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 38e70534a5..51c352a931 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -103,11 +103,11 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N @click.option('--install-app', multiple=True, help='Install app after installation') @click.option('--with-public-files', help='Restores the public files of the site, given path to its tar file') @click.option('--with-private-files', help='Restores the private files of the site, given path to its tar file') -@click.option('--force', is_flag=True, default=False, help='Ignore the site downgrade warning, if applicable') +@click.option('--force', is_flag=True, default=False, help='Ignore the validations and downgrade warnings. This action is not recommended') @pass_context def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None): "Restore site database from an sql file" - from frappe.installer import extract_sql_gzip, extract_files, is_downgrade + from frappe.installer import extract_sql_gzip, extract_files, is_downgrade, validate_database_sql force = context.force or force # Extract the gzip file if user has passed *.sql.gz file instead of *.sql file @@ -127,6 +127,7 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas else: decompressed_file_name = sql_file_path + validate_database_sql(decompressed_file_name, _raise=not force) site = get_site(context) frappe.init(site=site) @@ -222,15 +223,51 @@ def install_app(context, apps): sys.exit(exit_code) -@click.command('list-apps') +@click.command("list-apps") @pass_context def list_apps(context): "List apps in site" - site = get_site(context) - frappe.init(site=site) - frappe.connect() - print("\n".join(frappe.get_installed_apps())) - frappe.destroy() + + def fix_whitespaces(text): + if site == context.sites[-1]: + text = text.rstrip() + if len(context.sites) == 1: + text = text.lstrip() + return text + + for site in context.sites: + frappe.init(site=site) + frappe.connect() + site_title = ( + click.style(f"{site}", fg="green") if len(context.sites) > 1 else "" + ) + apps = frappe.get_single("Installed Applications").installed_applications + + if apps: + name_len, ver_len = [ + max([len(x.get(y)) for x in apps]) + for y in ["app_name", "app_version"] + ] + template = "{{0:{0}}} {{1:{1}}} {{2}}".format(name_len, ver_len) + + installed_applications = [ + template.format(app.app_name, app.app_version, app.git_branch) + for app in apps + ] + applications_summary = "\n".join(installed_applications) + summary = f"{site_title}\n{applications_summary}\n" + + else: + applications_summary = "\n".join(frappe.get_installed_apps()) + summary = f"{site_title}\n{applications_summary}\n" + + summary = fix_whitespaces(summary) + + if applications_summary and summary: + print(summary) + + frappe.destroy() + @click.command('add-system-manager') @click.argument('email') @@ -265,14 +302,12 @@ def disable_user(context, email): user.save(ignore_permissions=True) frappe.db.commit() - @click.command('migrate') @click.option('--skip-failing', is_flag=True, help="Skip patches that fail to run") @click.option('--skip-search-index', is_flag=True, help="Skip search indexing for web documents") @pass_context def migrate(context, skip_failing=False, skip_search_index=False): "Run patches, sync schema and rebuild files/translations" - import compileall import re from frappe.migrate import migrate @@ -291,9 +326,6 @@ def migrate(context, skip_failing=False, skip_search_index=False): if not context.sites: raise SiteNotSpecifiedError - print("Compiling Python files...") - compileall.compile_dir('../apps', quiet=1, rx=re.compile('.*node_modules.*')) - @click.command('migrate-to') @click.argument('frappe_provider') @pass_context @@ -310,15 +342,16 @@ def migrate_to(context, frappe_provider): @click.command('run-patch') @click.argument('module') +@click.option('--force', is_flag=True) @pass_context -def run_patch(context, module): +def run_patch(context, module, force): "Run a particular patch" import frappe.modules.patch_handler for site in context.sites: frappe.init(site=site) try: frappe.connect() - frappe.modules.patch_handler.run_single(module, force=context.force) + frappe.modules.patch_handler.run_single(module, force=force or context.force) finally: frappe.destroy() if not context.sites: diff --git a/frappe/config/customization.py b/frappe/config/customization.py index 3d587e6839..95fa5d355c 100644 --- a/frappe/config/customization.py +++ b/frappe/config/customization.py @@ -54,12 +54,6 @@ def get_data(): "label": _("Custom Translations"), "name": "Translation", "description": _("Add your own translations") - }, - { - "type": "doctype", - "label": _("Package"), - "name": "Package", - "description": _("Import and Export Packages.") } ] } diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index e420d3b775..ca134665b8 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -13,6 +13,7 @@ "fieldname", "precision", "length", + "non_negative", "hide_days", "hide_seconds", "reqd", @@ -473,13 +474,20 @@ "fieldname": "hide_border", "fieldtype": "Check", "label": "Hide Border" + }, + { + "default": "0", + "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)", + "fieldname": "non_negative", + "fieldtype": "Check", + "label": "Non Negative" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-08-28 11:28:21.252853", + "modified": "2020-10-29 06:09:26.454990", "modified_by": "Administrator", "module": "Core", "name": "DocField", diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 8a9c130fbe..fd0cb1917d 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -56,7 +56,8 @@ class DocType(Document): - Check fieldnames (duplication etc) - Clear permission table for child tables - Add `amended_from` and `amended_by` if Amendable - - Add custom field `auto_repeat` if Repeatable""" + - Add custom field `auto_repeat` if Repeatable + - Check if links point to valid fieldnames""" self.check_developer_mode() @@ -88,6 +89,7 @@ class DocType(Document): self.make_repeatable() self.validate_nestedset() self.validate_website() + self.validate_links_table_fieldnames() if not self.is_new(): self.before_update = frappe.get_doc('DocType', self.name) @@ -656,6 +658,19 @@ class DocType(Document): if not re.match("^(?![\W])[^\d_\s][\w ]+$", name, **flags): frappe.throw(_("DocType's name should start with a letter and it can only consist of letters, numbers, spaces and underscores"), frappe.NameError) + def validate_links_table_fieldnames(self): + """Validate fieldnames in Links table""" + if frappe.flags.in_patch: return + if frappe.flags.in_fixtures: return + if not self.links: return + + for index, link in enumerate(self.links): + meta = frappe.get_meta(link.link_doctype) + if not meta.get_field(link.link_fieldname): + message = _("Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype)) + frappe.throw(message, InvalidFieldNameError, _("Invalid Fieldname")) + + def validate_fields_for_doctype(doctype): doc = frappe.get_doc("DocType", doctype) diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index 6f4a400577..10169073e5 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -451,6 +451,33 @@ class TestDocType(unittest.TestCase): test_doc_1.delete() frappe.db.commit() + def test_links_table_fieldname_validation(self): + doc = new_doctype("Test Links Table Validation") + + # check valid data + doc.append("links", { + 'link_doctype': "User", + 'link_fieldname': "first_name" + }) + doc.validate_links_table_fieldnames() # no error + doc.links = [] # reset links table + + # check invalid doctype + doc.append("links", { + 'link_doctype': "User2", + 'link_fieldname': "first_name" + }) + self.assertRaises(frappe.DoesNotExistError, doc.validate_links_table_fieldnames) + doc.links = [] # reset links table + + # check invalid fieldname + doc.append("links", { + 'link_doctype': "User", + 'link_fieldname': "a_field_that_does_not_exists" + }) + self.assertRaises(InvalidFieldNameError, doc.validate_links_table_fieldnames) + + def new_doctype(name, unique=0, depends_on='', fields=None): doc = frappe.get_doc({ "doctype": "DocType", diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index 9d30409a2a..01c32bcb57 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -61,8 +61,9 @@ class Report(Document): def set_doctype_roles(self): if not self.get('roles') and self.is_standard == 'No': meta = frappe.get_meta(self.ref_doctype) - roles = [{'role': d.role} for d in meta.permissions if d.permlevel==0] - self.set('roles', roles) + if not meta.istable: + roles = [{'role': d.role} for d in meta.permissions if d.permlevel==0] + self.set('roles', roles) def is_permitted(self): """Returns true if Has Role is not set or the user is allowed.""" diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py index e458b401e4..1920189f78 100644 --- a/frappe/core/doctype/role/role.py +++ b/frappe/core/doctype/role/role.py @@ -37,7 +37,7 @@ class Role(Document): def get_info_based_on_role(role, field='email'): ''' Get information of all users that have been assigned this role ''' users = frappe.get_list("Has Role", filters={"role": role, "parenttype": "User"}, - fields=["parent"]) + fields=["parent as user_name"]) return get_user_info(users, field) @@ -45,7 +45,7 @@ def get_user_info(users, field='email'): ''' Fetch details about users for the specified field ''' info_list = [] for user in users: - user_info, enabled = frappe.db.get_value("User", user.parent, [field, "enabled"]) + user_info, enabled = frappe.db.get_value("User", user.get("user_name"), [field, "enabled"]) if enabled and user_info not in ["admin@example.com", "guest@example.com"]: info_list.append(user_info) return info_list diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index cc3995ad1d..420f96ec2f 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -31,6 +31,7 @@ "fieldname": "script", "fieldtype": "Code", "label": "Script", + "options": "Python", "reqd": 1 }, { @@ -87,7 +88,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-08-24 16:44:41.060350", + "modified": "2020-11-11 12:39:41.391052", "modified_by": "Administrator", "module": "Core", "name": "Server Script", diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json index 3946568bb6..2f0819ab68 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -30,6 +30,7 @@ "mandatory_depends_on", "read_only_depends_on", "properties", + "non_negative", "reqd", "unique", "read_only", @@ -403,13 +404,20 @@ "fieldname": "hide_border", "fieldtype": "Check", "label": "Hide Border" + }, + { + "default": "0", + "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)", + "fieldname": "non_negative", + "fieldtype": "Check", + "label": "Non Negative" } ], "icon": "fa fa-glass", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-08-28 11:28:44.377753", + "modified": "2020-10-29 06:14:43.073329", "modified_by": "Administrator", "module": "Custom", "name": "Custom Field", diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 61ecdd88b9..9ce602906c 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -488,6 +488,7 @@ docfield_properties = { 'permlevel': 'Int', 'width': 'Data', 'print_width': 'Data', + 'non_negative': 'Check', 'reqd': 'Check', 'unique': 'Check', 'ignore_user_permissions': 'Check', diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index 1d71e1d1e3..227114137c 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -11,6 +11,7 @@ "label", "fieldtype", "fieldname", + "non_negative", "reqd", "unique", "in_list_view", @@ -414,13 +415,20 @@ "fieldname": "hide_border", "fieldtype": "Check", "label": "Hide Border" + }, + { + "default": "0", + "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)", + "fieldname": "non_negative", + "fieldtype": "Check", + "label": "Non Negative" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-09-24 14:05:31.093927", + "modified": "2020-10-29 06:11:57.661039", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", diff --git a/frappe/custom/doctype/package_document_type/package_document_type.json b/frappe/custom/doctype/package_document_type/package_document_type.json deleted file mode 100644 index 6d011bd4e4..0000000000 --- a/frappe/custom/doctype/package_document_type/package_document_type.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "actions": [], - "creation": "2020-05-14 16:45:47.196395", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "document_type", - "column_break_2", - "attachments", - "overwrite", - "section_break_4", - "filters_json" - ], - "fields": [ - { - "fieldname": "document_type", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Document Type", - "options": "DocType", - "reqd": 1 - }, - { - "fieldname": "column_break_2", - "fieldtype": "Column Break" - }, - { - "default": "0", - "fieldname": "attachments", - "fieldtype": "Check", - "in_list_view": 1, - "label": "Include Attachments" - }, - { - "default": "0", - "fieldname": "overwrite", - "fieldtype": "Check", - "in_list_view": 1, - "label": "Overwrite" - }, - { - "fieldname": "section_break_4", - "fieldtype": "Section Break" - }, - { - "fieldname": "filters_json", - "fieldtype": "Code", - "label": "Filters", - "options": "JSON" - } - ], - "istable": 1, - "links": [], - "modified": "2020-05-14 16:45:47.196395", - "modified_by": "Administrator", - "module": "Custom", - "name": "Package Document Type", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/frappe/custom/doctype/package_document_type/package_document_type.py b/frappe/custom/doctype/package_document_type/package_document_type.py deleted file mode 100644 index 6e166eecbd..0000000000 --- a/frappe/custom/doctype/package_document_type/package_document_type.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, 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 PackageDocumentType(Document): - pass diff --git a/frappe/custom/doctype/package_publish_target/__init__.py b/frappe/custom/doctype/package_publish_target/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/custom/doctype/package_publish_target/package_publish_target.json b/frappe/custom/doctype/package_publish_target/package_publish_target.json deleted file mode 100644 index baeb7cb8bc..0000000000 --- a/frappe/custom/doctype/package_publish_target/package_publish_target.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "actions": [], - "creation": "2020-05-13 16:04:32.724663", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "instance_url", - "username", - "password" - ], - "fields": [ - { - "fieldname": "instance_url", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Site URL", - "reqd": 1 - }, - { - "fieldname": "username", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Username", - "reqd": 1 - }, - { - "fieldname": "password", - "fieldtype": "Password", - "in_list_view": 1, - "label": "Password", - "reqd": 1 - } - ], - "istable": 1, - "links": [], - "modified": "2020-05-15 17:35:16.282235", - "modified_by": "Administrator", - "module": "Custom", - "name": "Package Publish Target", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/frappe/custom/doctype/package_publish_tool/__init__.py b/frappe/custom/doctype/package_publish_tool/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/custom/doctype/package_publish_tool/package_publish_tool.js b/frappe/custom/doctype/package_publish_tool/package_publish_tool.js deleted file mode 100644 index a0190a8d8c..0000000000 --- a/frappe/custom/doctype/package_publish_tool/package_publish_tool.js +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) 2020, Frappe Technologies and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Package Publish Tool', { - refresh: function(frm) { - frm.set_query("document_type", "package_details", function () { - return { - filters: { - "istable": 0, - } - }; - }); - - frappe.realtime.on("package", (data) => { - frm.dashboard.show_progress(data.prefix, data.progress / data.total * 100, __("{0}", [data.message])); - if ((data.progress+1) != data.total) { - frm.dashboard.show_progress(data.prefix, data.progress / data.total * 100, __("{0}", [data.message])); - } else { - frm.dashboard.hide_progress(); - } - }); - - frm.trigger("show_instructions"); - frm.trigger("last_deployed_on"); - frm.trigger("set_dirty_trigger"); - frm.trigger("set_deploy_primary_action"); - }, - last_deployed_on: function(frm) { - if (frm.doc.last_deployed_on) { - frm.trigger("show_indicator"); - } - }, - show_indicator: function(frm) { - let pretty_date = frappe.datetime.prettyDate(frm.doc.last_deployed_on); - frm.page.set_indicator(__("Last published {0}", [pretty_date]), "blue"); - }, - set_dirty_trigger: function(frm) { - $(frm.wrapper).on("dirty", function() { - frm.page.set_primary_action(__('Save'), () => frm.save()); - }); - }, - set_deploy_primary_action: function(frm) { - if (frm.doc.package_details.length && frm.doc.instances.length) { - frm.page.set_primary_action(__("Publish"), function () { - frappe.show_alert({ - message: __("Publishing documents..."), - indicator: "green" - }); - - frappe.call({ - method: "frappe.custom.doctype.package_publish_tool.package_publish_tool.deploy_package", - callback: function() { - frm.reload_doc(); - frappe.msgprint(__("Documents have been published.")); - } - }); - }); - } - }, - show_instructions: function(frm) { - let field = frm.get_field("html_info"); - field.html(` -

- Package Publish Tool let's you copy documents from your site to any other remote site. - Follow the steps below to publish. -

-
    -
  1. Add Document Types that you want to copy from the table below. You can also add filters by expanding the row.
  2. -
  3. Add the Sites URL where you want to copy these documents, and enter the Username and Password.
  4. -
  5. Click on Save. Now, you can click on Publish and the documents will be copied.
  6. -
- `); - } -}); - -frappe.ui.form.on('Package Document Type', { - form_render: function (frm, cdt, cdn) { - function _show_filters(filters, table) { - table.find('tbody').empty(); - - if (filters.length > 0) { - filters.forEach(filter => { - const filter_row = - $(` - ${filter[1]} - ${filter[2] || ""} - ${filter[3]} - `); - - table.find('tbody').append(filter_row); - }); - } else { - const filter_row = $(` - ${__("Click to Set Filters")}`); - table.find('tbody').append(filter_row); - } - } - - let row = frappe.get_doc(cdt, cdn); - - let wrapper = $(`[data-fieldname="filters_json"]`).empty(); - let table = $(` - - - - - - - - - -
${__('Filter')}${__('Condition')}${__('Value')}
`).appendTo(wrapper); - $(`

${__("Click table to edit")}

`).appendTo(wrapper); - - let filters = JSON.parse(row.filters_json || '[]'); - _show_filters(filters, table); - - table.on('click', () => { - if (!row.document_type) { - frappe.msgprint(__("Select Document Type.")); - return; - } - - frappe.model.with_doctype(row.document_type, function() { - let dialog = new frappe.ui.Dialog({ - title: __('Set Filters'), - fields: [ - { - fieldtype: 'HTML', - label: 'Filters', - fieldname: 'filter_area', - } - ], - primary_action: function() { - let values = filter_group.get_filters(); - let flt = []; - if (values) { - values.forEach(function(value) { - flt.push([value[0], value[1], value[2], value[3]]); - }); - } - row.filters_json = JSON.stringify(flt); - _show_filters(flt, table); - dialog.hide(); - }, - primary_action_label: "Set" - }); - - let filter_group = new frappe.ui.FilterGroup({ - parent: dialog.get_field('filter_area').$wrapper, - doctype: row.document_type, - on_change: () => {}, - }); - filter_group.add_filters_to_filter_group(filters); - dialog.show(); - }); - }); - }, -}); diff --git a/frappe/custom/doctype/package_publish_tool/package_publish_tool.json b/frappe/custom/doctype/package_publish_tool/package_publish_tool.json deleted file mode 100644 index 0f85ae0348..0000000000 --- a/frappe/custom/doctype/package_publish_tool/package_publish_tool.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "actions": [], - "creation": "2020-05-13 15:54:38.082657", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "html_info", - "sb_00", - "package_details", - "sb_01", - "instances", - "last_deployed_on" - ], - "fields": [ - { - "description": "Click on the row for accessing filters.", - "fieldname": "package_details", - "fieldtype": "Table", - "label": "Document Types", - "options": "Package Document Type", - "reqd": 1 - }, - { - "fieldname": "instances", - "fieldtype": "Table", - "label": "Sites", - "options": "Package Publish Target", - "reqd": 1 - }, - { - "fieldname": "html_info", - "fieldtype": "HTML" - }, - { - "fieldname": "last_deployed_on", - "fieldtype": "Datetime", - "hidden": 1, - "label": "Last Deployed On", - "read_only": 1 - }, - { - "fieldname": "sb_00", - "fieldtype": "Section Break" - }, - { - "fieldname": "sb_01", - "fieldtype": "Section Break" - } - ], - "issingle": 1, - "links": [], - "modified": "2020-05-15 17:31:37.060199", - "modified_by": "Administrator", - "module": "Custom", - "name": "Package Publish Tool", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "All", - "share": 1, - "write": 1 - } - ], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/frappe/custom/doctype/package_publish_tool/package_publish_tool.py b/frappe/custom/doctype/package_publish_tool/package_publish_tool.py deleted file mode 100644 index b73f93a628..0000000000 --- a/frappe/custom/doctype/package_publish_tool/package_publish_tool.py +++ /dev/null @@ -1,178 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -import json -import datetime -import base64 -from frappe.model.document import Document -from frappe.utils.file_manager import save_file, get_file -from frappe import _ -from six import string_types -from frappe.frappeclient import FrappeClient -from frappe.utils import get_datetime_str, get_datetime -from frappe.utils.password import get_decrypted_password - -class PackagePublishTool(Document): - pass - -@frappe.whitelist() -def deploy_package(): - package, doc = export_package() - - file_name = "Package-" + get_datetime_str(get_datetime()) - - length = len(doc.instances) - for idx, instance in enumerate(doc.instances): - frappe.publish_realtime("package", {"progress": idx, "total": length, "message": instance.instance_url, "prefix": _("Deploying")}, - user=frappe.session.user) - - install_package_to_remote(package, instance) - - frappe.db.set_value("Package Publish Tool", "Package Publish Tool", "last_deployed_on", frappe.utils.now_datetime()) - -def install_package_to_remote(package, instance): - try: - connection = FrappeClient(instance.instance_url, instance.username, get_decrypted_password(instance.doctype, instance.name)) - except Exception: - frappe.log_error(frappe.get_traceback()) - frappe.throw(_("Couldn't connect to site {0}. Please check Error Logs.").format(instance.instance_url)) - - try: - connection.post_request({ - "cmd": "frappe.custom.doctype.package_publish_tool.package_publish_tool.import_package", - "package": json.dumps(package) - }) - except Exception: - frappe.log_error(frappe.get_traceback()) - frappe.throw(_("Error while installing package to site {0}. Please check Error Logs.").format(instance.instance_url)) - -@frappe.whitelist() -def export_package(): - """Export package as JSON.""" - package_doc = frappe.get_single("Package Publish Tool") - package = [] - - for doctype in package_doc.package_details: - filters = [] - - if doctype.get("filters_json"): - filters = json.loads(doctype.get("filters_json")) - - docs = frappe.get_all(doctype.get("document_type"), filters=filters) - length = len(docs) - - for idx, doc in enumerate(docs): - frappe.publish_realtime("package", { - "progress":idx, "total":length, - "message":doctype.get("document_type"), - "prefix": _("Exporting") - }, - user=frappe.session.user) - - document = frappe.get_doc(doctype.get("document_type"), doc.name).as_dict() - attachments = [] - - if doctype.attachments: - filters = { - "attached_to_doctype": document.get("doctype"), - "attached_to_name": document.get("name") - } - - for f in frappe.get_list("File", filters=filters): - fname, fcontents = get_file(f.name) - attachments.append({ - "fname": fname, - "content": base64.b64encode(fcontents).decode('ascii') - }) - - document.update({ - "__attachments": attachments, - "__overwrite": True if doctype.overwrite else False - }) - - package.append(document) - - return post_process(package), package_doc - -@frappe.whitelist() -def import_package(package=None): - """Import package from JSON.""" - frappe.only_for("System Manager") - if isinstance(package, string_types): - package = json.loads(package) - - for doc in package: - modified = doc.pop("modified") - overwrite = doc.pop("__overwrite") - attachments = doc.pop("__attachments") - exists = frappe.db.exists(doc.get("doctype"), doc.get("name")) - - if not exists: - d = frappe.get_doc(doc).insert(ignore_permissions=True, ignore_if_duplicate=True) - if attachments: - add_attachment(attachments, d) - else: - docname = doc.pop("name") - document = frappe.get_doc(doc.get("doctype"), docname) - - if overwrite: - update_document(document, doc, attachments) - - else: - if frappe.utils.get_datetime(document.modified) < frappe.utils.get_datetime(modified): - update_document(document, doc, attachments) - -def update_document(document, doc, attachments): - document.update(doc) - document.save() - if attachments: - add_attachment(attachments, document) - -def add_attachment(attachments, doc): - for attachment in attachments: - save_file(attachment.get("fname"), base64.b64decode(attachment.get("content")), doc.get("doctype"), doc.get("name")) - -def post_process(package): - """Remove the keys from Document and Child Document. Convert datetime, date, time to str.""" - del_keys = ('modified_by', 'creation', 'owner', 'idx', 'docstatus') - child_del_keys = ('modified_by', 'creation', 'owner', 'idx', 'docstatus', 'name') - - for doc in package: - for key in del_keys: - if key in doc: - del doc[key] - - for key, value in doc.items(): - stringified_value = get_stringified_value(value) - if stringified_value: - doc[key] = stringified_value - - if not isinstance(value, list): - continue - - for child in value: - for child_key in child_del_keys: - if child_key in child: - del child[child_key] - - for child_key, child_value in child.items(): - stringified_value = get_stringified_value(child_value) - if stringified_value: - child[child_key] = stringified_value - - return package - -def get_stringified_value(value): - if isinstance(value, datetime.datetime): - return frappe.utils.get_datetime_str(value) - - if isinstance(value, datetime.date): - return frappe.utils.get_date_str(value) - - if isinstance(value, datetime.timedelta): - return frappe.utils.get_time_str(value) - - return None diff --git a/frappe/custom/doctype/package_publish_tool/test_package_publish_tool.py b/frappe/custom/doctype/package_publish_tool/test_package_publish_tool.py deleted file mode 100644 index 8332240543..0000000000 --- a/frappe/custom/doctype/package_publish_tool/test_package_publish_tool.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies and Contributors -# See license.txt -from __future__ import unicode_literals - -# import frappe -import unittest - -class TestPackagePublishTool(unittest.TestCase): - pass diff --git a/frappe/email/doctype/notification/notification.js b/frappe/email/doctype/notification/notification.js index 2cc027acd6..27fcd0e453 100644 --- a/frappe/email/doctype/notification/notification.js +++ b/frappe/email/doctype/notification/notification.js @@ -97,14 +97,7 @@ frappe.notification = { }, setup_example_message: function(frm) { let template = ''; - if (frm.doc.channel === 'WhatsApp') { - template = `
Warning:
Only Use Pre-Approved WhatsApp for Business Template -
Message Example
- -
-Your appointment is coming up on {{ doc.date }} at {{ doc.time }}
-
`; - } else if (frm.doc.channel === 'Email') { + if (frm.doc.channel === 'Email') { template = `
Message Example
<h3>Order Overdue</h3>
@@ -124,7 +117,7 @@ Last comment: {{ comments[-1].comment }} by {{ comments[-1].by }}
 </ul>
 
`; - } else { + } else if (in_list(['Slack', 'System Notification', 'SMS'], frm.doc.channel)) { template = `
Message Example
*Order Overdue*
@@ -142,7 +135,9 @@ Last comment: {{ comments[-1].comment }} by {{ comments[-1].by }}
 • Amount: {{ doc.grand_total }}
 
`; } - frm.set_df_property('message_examples', 'options', template); + if (template) { + frm.set_df_property('message_examples', 'options', template); + } } }; diff --git a/frappe/email/doctype/notification/notification.json b/frappe/email/doctype/notification/notification.json index 2a8ee1aeb1..73a84e1d3e 100644 --- a/frappe/email/doctype/notification/notification.json +++ b/frappe/email/doctype/notification/notification.json @@ -10,7 +10,6 @@ "enabled", "column_break_2", "channel", - "twilio_number", "slack_webhook_url", "filters", "subject", @@ -61,7 +60,7 @@ "fieldname": "channel", "fieldtype": "Select", "label": "Channel", - "options": "Email\nSlack\nSystem Notification\nWhatsApp\nSMS", + "options": "Email\nSlack\nSystem Notification\nSMS", "reqd": 1, "set_only_once": 1 }, @@ -80,14 +79,14 @@ "label": "Filters" }, { - "depends_on": "eval: !in_list(['SMS', 'WhatsApp'], doc.channel)", + "depends_on": "eval: in_list(['Email', 'Slack', 'System Notification'], doc.channel)", "description": "To add dynamic subject, use jinja tags like\n\n
{{ doc.name }} Delivered
", "fieldname": "subject", "fieldtype": "Data", "ignore_xss_filter": 1, "in_list_view": 1, "label": "Subject", - "mandatory_depends_on": "eval:!in_list(['SMS', 'WhatsApp'], doc.channel)" + "mandatory_depends_on": "eval: in_list(['Email', 'Slack', 'System Notification'], doc.channel)" }, { "fieldname": "document_type", @@ -208,7 +207,7 @@ "label": "Value To Be Set" }, { - "depends_on": "eval:in_list(['Email', 'SMS', 'WhatsApp'], doc.channel)", + "depends_on": "eval:in_list(['Email', 'SMS'], doc.channel)", "fieldname": "column_break_5", "fieldtype": "Section Break", "label": "Recipients" @@ -263,15 +262,6 @@ "label": "Print Format", "options": "Print Format" }, - { - "depends_on": "eval: doc.channel==='WhatsApp'", - "description": "To use WhatsApp for Business, initialize Twilio Settings.", - "fieldname": "twilio_number", - "fieldtype": "Link", - "label": "Twilio Number", - "mandatory_depends_on": "eval: doc.channel==='WhatsApp'", - "options": "Twilio Number Group" - }, { "default": "0", "depends_on": "eval: doc.channel !== 'System Notification'", @@ -291,7 +281,7 @@ "icon": "fa fa-envelope", "index_web_pages_for_search": 1, "links": [], - "modified": "2020-09-03 10:33:23.084590", + "modified": "2020-10-28 11:04:54.955567", "modified_by": "Administrator", "module": "Email", "name": "Notification", diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index 62be313b82..75281d427e 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -14,7 +14,6 @@ from frappe.utils.safe_exec import get_safe_globals from frappe.modules.utils import export_module_json, get_doc_module from six import string_types from frappe.integrations.doctype.slack_webhook_url.slack_webhook_url import send_slack_message -from frappe.integrations.doctype.twilio_settings.twilio_settings import send_whatsapp_message from frappe.core.doctype.sms_settings.sms_settings import send_sms from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification @@ -29,7 +28,7 @@ class Notification(Document): self.name = self.subject def validate(self): - if self.channel not in ('WhatsApp', 'SMS'): + if self.channel in ("Email", "Slack", "System Notification"): validate_template(self.subject) validate_template(self.message) @@ -43,7 +42,6 @@ class Notification(Document): self.validate_forbidden_types() self.validate_condition() self.validate_standard() - self.validate_twilio_settings() frappe.cache().hdel('notifications', self.document_type) def on_update(self): @@ -70,11 +68,6 @@ def get_context(context): if self.is_standard and not frappe.conf.developer_mode: frappe.throw(_('Cannot edit Standard Notification. To edit, please disable this and duplicate it')) - def validate_twilio_settings(self): - if self.enabled and self.channel == "WhatsApp" \ - and not frappe.db.get_single_value("Twilio Settings", "enabled"): - frappe.throw(_("Please enable Twilio settings to send WhatsApp messages")) - def validate_condition(self): temp_doc = frappe.new_doc(self.document_type) if self.condition: @@ -137,9 +130,6 @@ def get_context(context): if self.channel == 'Slack': self.send_a_slack_msg(doc, context) - if self.channel == 'WhatsApp': - self.send_whatsapp_msg(doc, context) - if self.channel == 'SMS': self.send_sms(doc, context) @@ -230,13 +220,6 @@ def get_context(context): reference_doctype=doc.doctype, reference_name=doc.name) - def send_whatsapp_msg(self, doc, context): - send_whatsapp_message( - sender=self.twilio_number, - receiver_list=self.get_receiver_list(doc, context), - message=frappe.render_template(self.message, context), - ) - def send_sms(self, doc, context): send_sms( receiver_list=self.get_receiver_list(doc, context), @@ -302,7 +285,7 @@ def get_context(context): # For sending messages to the owner's mobile phone number if recipient.receiver_by_document_field == 'owner': - receiver_list.append(get_user_info(doc.get('owner'), 'mobile_no')) + receiver_list += get_user_info([dict(user_name=doc.get('owner'))], 'mobile_no') # For sending messages to the number specified in the receiver field elif recipient.receiver_by_document_field: receiver_list.append(doc.get(recipient.receiver_by_document_field)) diff --git a/frappe/event_streaming/doctype/event_consumer/event_consumer.py b/frappe/event_streaming/doctype/event_consumer/event_consumer.py index 1505c3a05d..5789e09e74 100644 --- a/frappe/event_streaming/doctype/event_consumer/event_consumer.py +++ b/frappe/event_streaming/doctype/event_consumer/event_consumer.py @@ -31,10 +31,12 @@ class EventConsumer(Document): self.update_consumer_status() else: frappe.db.set_value(self.doctype, self.name, 'incoming_change', 0) - + frappe.cache().delete_value('event_consumer_document_type_map') def on_trash(self): + for i in frappe.get_all('Event Update Log Consumer', {'consumer': self.name}): + frappe.delete_doc('Event Update Log Consumer', i.name) frappe.cache().delete_value('event_consumer_document_type_map') def update_consumer_status(self): @@ -88,8 +90,9 @@ def register_consumer(data): for entry in consumer_doctypes: consumer.append('consumer_doctypes', { - 'ref_doctype': entry, - 'status': 'Pending' + 'ref_doctype': entry.get('doctype'), + 'status': 'Pending', + 'condition': entry.get('condition') }) consumer.insert() @@ -153,3 +156,53 @@ def notify(consumer): jobs = get_jobs() if not jobs or enqueued_method not in jobs[frappe.local.site] and not consumer.flags.notifed: frappe.enqueue(enqueued_method, queue='long', enqueue_after_commit=True, **{'consumer': consumer}) + + +def has_consumer_access(consumer, update_log): + """Checks if consumer has completely satisfied all the conditions on the doc""" + + if isinstance(consumer, str): + consumer = frappe.get_doc('Event Consumer', consumer) + + if not frappe.db.exists(update_log.ref_doctype, update_log.docname): + # Delete Log + # Check if the last Update Log of this document was read by this consumer + last_update_log = frappe.get_all( + 'Event Update Log', + filters={ + 'ref_doctype': update_log.ref_doctype, + 'docname': update_log.docname, + 'creation': ['<', update_log.creation] + }, + order_by='creation desc', + limit_page_length=1 + ) + if not len(last_update_log): + return False + + last_update_log = frappe.get_doc('Event Update Log', last_update_log[0].name) + return len([x for x in last_update_log.consumers if x.consumer == consumer.name]) + + doc = frappe.get_doc(update_log.ref_doctype, update_log.docname) + try: + for dt_entry in consumer.consumer_doctypes: + if dt_entry.ref_doctype != update_log.ref_doctype: + continue + + if not dt_entry.condition: + return True + + condition: str = dt_entry.condition + if condition.startswith('cmd:'): + cmd = condition.split('cmd:')[1].strip() + args = { + 'consumer': consumer, + 'doc': doc, + 'update_log': update_log + } + return frappe.call(cmd, **args) + else: + return frappe.safe_eval(condition, frappe._dict(doc=doc)) + except Exception as e: + frappe.log_error(title='has_consumer_access error', message=e) + return False \ No newline at end of file diff --git a/frappe/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.json b/frappe/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.json index 71dcc63127..c243334a09 100644 --- a/frappe/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.json +++ b/frappe/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.json @@ -7,7 +7,8 @@ "field_order": [ "ref_doctype", "status", - "unsubscribed" + "unsubscribed", + "condition" ], "fields": [ { @@ -37,11 +38,17 @@ "in_list_view": 1, "label": "Unsubscribed", "read_only": 1 + }, + { + "fieldname": "condition", + "fieldtype": "Code", + "label": "Condition", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2020-08-14 12:38:40.918620", + "modified": "2020-11-07 09:26:49.894294", "modified_by": "Administrator", "module": "Event Streaming", "name": "Event Consumer Document Type", diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.py b/frappe/event_streaming/doctype/event_producer/event_producer.py index d458f3c24b..d8a6a55510 100644 --- a/frappe/event_streaming/doctype/event_producer/event_producer.py +++ b/frappe/event_streaming/doctype/event_producer/event_producer.py @@ -102,9 +102,13 @@ class EventProducer(Document): for entry in self.producer_doctypes: if entry.has_mapping: # if mapping, subscribe to remote doctype on consumer's site - consumer_doctypes.append(frappe.db.get_value('Document Type Mapping', entry.mapping, 'remote_doctype')) + dt = frappe.db.get_value('Document Type Mapping', entry.mapping, 'remote_doctype') else: - consumer_doctypes.append(entry.ref_doctype) + dt = entry.ref_doctype + consumer_doctypes.append({ + "doctype": dt, + "condition": entry.condition + }) user_key = frappe.db.get_value('User', self.user, 'api_key') user_secret = get_decrypted_password('User', self.user, 'api_secret') @@ -145,7 +149,8 @@ class EventProducer(Document): event_consumer.consumer_doctypes.append({ 'ref_doctype': ref_doctype, 'status': get_approval_status(config, ref_doctype), - 'unsubscribed': entry.unsubscribe + 'unsubscribed': entry.unsubscribe, + 'condition': entry.condition }) event_consumer.user = self.user event_consumer.incoming_change = True @@ -347,13 +352,13 @@ def set_delete(update): def get_updates(producer_site, last_update, doctypes): """Get all updates generated after the last update timestamp""" - docs = producer_site.get_list( - doctype='Event Update Log', - filters={'ref_doctype': ('in', doctypes), 'creation': ('>', last_update)}, - fields=['update_type', 'ref_doctype', 'docname', 'data', 'name', 'creation'] - ) - docs.reverse() - return [frappe._dict(d) for d in docs] + docs = producer_site.post_request({ + 'cmd': 'frappe.event_streaming.doctype.event_update_log.event_update_log.get_update_logs_for_consumer', + 'event_consumer': get_url(), + 'doctypes': frappe.as_json(doctypes), + 'last_update': last_update + }) + return [frappe._dict(d) for d in (docs or [])] def get_local_doc(update): diff --git a/frappe/event_streaming/doctype/event_producer/test_event_producer.py b/frappe/event_streaming/doctype/event_producer/test_event_producer.py index fa2461a9d8..4c259c3729 100644 --- a/frappe/event_streaming/doctype/event_producer/test_event_producer.py +++ b/frappe/event_streaming/doctype/event_producer/test_event_producer.py @@ -152,6 +152,82 @@ class TestEventProducer(unittest.TestCase): reset_configuration(producer_url) + def test_conditional_events(self): + producer = get_remote_site() + + # Add Condition + event_producer = frappe.get_doc('Event Producer', producer_url) + note_producer_entry = [ + x for x in event_producer.producer_doctypes if x.ref_doctype == 'Note' + ][0] + note_producer_entry.condition = 'doc.public == 1' + event_producer.save() + + # Make test doc + producer_note1 = frappe._dict(doctype='Note', public=0, title='test conditional sync') + delete_on_remote_if_exists(producer, 'Note', {'title': producer_note1['title']}) + producer_note1 = producer.insert(producer_note1) + + # Make Update + producer_note1['content'] = 'Test Conditional Sync Content' + producer_note1 = producer.update(producer_note1) + + self.pull_producer_data() + + # Check if synced here + self.assertFalse(frappe.db.exists('Note', producer_note1.name)) + + # Lets satisfy the condition + producer_note1['public'] = 1 + producer_note1 = producer.update(producer_note1) + + self.pull_producer_data() + + # it should sync now + self.assertTrue(frappe.db.exists('Note', producer_note1.name)) + local_note = frappe.get_doc('Note', producer_note1.name) + self.assertEqual(local_note.content, producer_note1.content) + + reset_configuration(producer_url) + + def test_conditional_events_with_cmd(self): + producer = get_remote_site() + + # Add Condition + event_producer = frappe.get_doc('Event Producer', producer_url) + note_producer_entry = [ + x for x in event_producer.producer_doctypes if x.ref_doctype == 'Note' + ][0] + note_producer_entry.condition = 'cmd: frappe.event_streaming.doctype.event_producer.test_event_producer.can_sync_note' + event_producer.save() + + # Make test doc + producer_note1 = frappe._dict(doctype='Note', public=0, title='test conditional sync cmd') + delete_on_remote_if_exists(producer, 'Note', {'title': producer_note1['title']}) + producer_note1 = producer.insert(producer_note1) + + # Make Update + producer_note1['content'] = 'Test Conditional Sync Content' + producer_note1 = producer.update(producer_note1) + + self.pull_producer_data() + + # Check if synced here + self.assertFalse(frappe.db.exists('Note', producer_note1.name)) + + # Lets satisfy the condition + producer_note1['public'] = 1 + producer_note1 = producer.update(producer_note1) + + self.pull_producer_data() + + # it should sync now + self.assertTrue(frappe.db.exists('Note', producer_note1.name)) + local_note = frappe.get_doc('Note', producer_note1.name) + self.assertEqual(local_note.content, producer_note1.content) + + reset_configuration(producer_url) + def test_update_log(self): producer = get_remote_site() producer_doc = insert_into_producer(producer, 'test update log') @@ -221,6 +297,8 @@ class TestEventProducer(unittest.TestCase): reset_configuration(producer_url) +def can_sync_note(consumer, doc, update_log): + return doc.public == 1 def setup_event_producer_for_inner_mapping(): event_producer = frappe.get_doc('Event Producer', producer_url, for_update=True) @@ -322,6 +400,7 @@ def create_event_producer(producer_url): def reset_configuration(producer_url): event_producer = frappe.get_doc('Event Producer', producer_url, for_update=True) event_producer.producer_doctypes = [] + event_producer.conditions = [] event_producer.producer_url = producer_url event_producer.append('producer_doctypes', { 'ref_doctype': 'ToDo', diff --git a/frappe/event_streaming/doctype/event_producer_document_type/event_producer_document_type.json b/frappe/event_streaming/doctype/event_producer_document_type/event_producer_document_type.json index e5fe9497f8..17fd51d12d 100644 --- a/frappe/event_streaming/doctype/event_producer_document_type/event_producer_document_type.json +++ b/frappe/event_streaming/doctype/event_producer_document_type/event_producer_document_type.json @@ -10,7 +10,8 @@ "use_same_name", "unsubscribe", "has_mapping", - "mapping" + "mapping", + "condition" ], "fields": [ { @@ -63,11 +64,16 @@ "fieldtype": "Check", "in_list_view": 1, "label": "Unsubscribe" + }, + { + "fieldname": "condition", + "fieldtype": "Code", + "label": "Condition" } ], "istable": 1, "links": [], - "modified": "2020-08-14 11:38:01.278996", + "modified": "2020-11-07 09:26:58.463868", "modified_by": "Administrator", "module": "Event Streaming", "name": "Event Producer Document Type", diff --git a/frappe/event_streaming/doctype/event_update_log/event_update_log.json b/frappe/event_streaming/doctype/event_update_log/event_update_log.json index 452a656b8b..a42bc7ec87 100644 --- a/frappe/event_streaming/doctype/event_update_log/event_update_log.json +++ b/frappe/event_streaming/doctype/event_update_log/event_update_log.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2019-07-30 15:31:26.352527", "doctype": "DocType", "editable_grid": 1, @@ -7,7 +8,8 @@ "update_type", "ref_doctype", "docname", - "data" + "data", + "consumers" ], "fields": [ { @@ -31,7 +33,6 @@ "fieldtype": "Data", "in_list_view": 1, "label": "Document Name", - "options": "ref_doctype", "read_only": 1 }, { @@ -39,10 +40,18 @@ "fieldtype": "Code", "label": "Data", "read_only": 1 + }, + { + "fieldname": "consumers", + "fieldtype": "Table MultiSelect", + "label": "Consumers", + "options": "Event Update Log Consumer", + "read_only": 1 } ], "in_create": 1, - "modified": "2019-09-24 23:16:07.207707", + "links": [], + "modified": "2020-09-04 07:31:52.599804", "modified_by": "Administrator", "module": "Event Streaming", "name": "Event Update Log", diff --git a/frappe/event_streaming/doctype/event_update_log/event_update_log.py b/frappe/event_streaming/doctype/event_update_log/event_update_log.py index 646331a02c..1c31718c2b 100644 --- a/frappe/event_streaming/doctype/event_update_log/event_update_log.py +++ b/frappe/event_streaming/doctype/event_update_log/event_update_log.py @@ -140,3 +140,137 @@ def check_docstatus(out, old, new, for_child): if not for_child and old.docstatus != new.docstatus: out.changed['docstatus'] = new.docstatus return out + + +def is_consumer_uptodate(update_log, consumer): + """ + Checks if Consumer has read all the UpdateLogs before the specified update_log + :param update_log: The UpdateLog Doc in context + :param consumer: The EventConsumer doc + """ + if update_log.update_type == 'Create': + # consumer is obviously up to date + return True + + prev_logs = frappe.get_all( + 'Event Update Log', + filters={ + 'ref_doctype': update_log.ref_doctype, + 'docname': update_log.docname, + 'creation': ['<', update_log.creation] + }, + order_by='creation desc', + limit_page_length=1 + ) + + if not len(prev_logs): + return False + + prev_log_consumers = frappe.get_all( + 'Event Update Log Consumer', + fields=['consumer'], + filters={ + 'parent': prev_logs[0].name, + 'parenttype': 'Event Update Log', + 'consumer': consumer.name + } + ) + + return len(prev_log_consumers) > 0 + + +def mark_consumer_read(update_log_name, consumer_name): + """ + This function appends the Consumer to the list of Consumers that has 'read' an Update Log + """ + update_log = frappe.get_doc('Event Update Log', update_log_name) + if len([x for x in update_log.consumers if x.consumer == consumer_name]): + return + + frappe.get_doc(frappe._dict( + doctype='Event Update Log Consumer', + consumer=consumer_name, + parent=update_log_name, + parenttype='Event Update Log', + parentfield='consumers' + )).insert(ignore_permissions=True) + + +def get_unread_update_logs(consumer_name, dt, dn): + """ + Get old logs unread by the consumer on a particular document + """ + already_consumed = [x[0] for x in frappe.db.sql(""" + SELECT + update_log.name + FROM `tabEvent Update Log` update_log + JOIN `tabEvent Update Log Consumer` consumer ON consumer.parent = update_log.name + WHERE + consumer.consumer = %(consumer)s + AND update_log.ref_doctype = %(dt)s + AND update_log.docname = %(dn)s + """, {'consumer': consumer_name, "dt": dt, "dn": dn}, as_dict=0)] + + logs = frappe.get_all( + 'Event Update Log', + fields=['update_type', 'ref_doctype', + 'docname', 'data', 'name', 'creation'], + filters={ + 'ref_doctype': dt, + 'docname': dn, + 'name': ['not in', already_consumed] + }, + order_by='creation' + ) + + return logs + + +@frappe.whitelist() +def get_update_logs_for_consumer(event_consumer, doctypes, last_update): + """ + Fetches all the UpdateLogs for the consumer + It will inject old un-consumed Update Logs if a doc was just found to be accessible to the Consumer + """ + + if isinstance(doctypes, str): + doctypes = frappe.parse_json(doctypes) + + from frappe.event_streaming.doctype.event_consumer.event_consumer import has_consumer_access + + consumer = frappe.get_doc('Event Consumer', event_consumer) + docs = frappe.get_list( + doctype='Event Update Log', + filters={'ref_doctype': ('in', doctypes), + 'creation': ('>', last_update)}, + fields=['update_type', 'ref_doctype', + 'docname', 'data', 'name', 'creation'], + order_by='creation desc' + ) + + result = [] + to_update_history = [] + for d in docs: + if (d.ref_doctype, d.docname) in to_update_history: + # will be notified by background jobs + continue + + if not has_consumer_access(consumer=consumer, update_log=d): + continue + + if not is_consumer_uptodate(d, consumer): + to_update_history.append((d.ref_doctype, d.docname)) + # get_unread_update_logs will have the current log + old_logs = get_unread_update_logs(consumer.name, d.ref_doctype, d.docname) + if old_logs: + old_logs.reverse() + result.extend(old_logs) + else: + result.append(d) + + + for d in result: + mark_consumer_read(update_log_name=d.name, consumer_name=consumer.name) + + result.reverse() + return result \ No newline at end of file diff --git a/frappe/custom/doctype/package_document_type/__init__.py b/frappe/event_streaming/doctype/event_update_log_consumer/__init__.py similarity index 100% rename from frappe/custom/doctype/package_document_type/__init__.py rename to frappe/event_streaming/doctype/event_update_log_consumer/__init__.py diff --git a/frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.json b/frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.json new file mode 100644 index 0000000000..b3484c6481 --- /dev/null +++ b/frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.json @@ -0,0 +1,32 @@ +{ + "actions": [], + "creation": "2020-06-30 10:54:53.301787", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "consumer" + ], + "fields": [ + { + "fieldname": "consumer", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Consumer", + "options": "Event Consumer", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-06-30 10:54:53.301787", + "modified_by": "Administrator", + "module": "Event Streaming", + "name": "Event Update Log Consumer", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/custom/doctype/package_publish_target/package_publish_target.py b/frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.py similarity index 85% rename from frappe/custom/doctype/package_publish_target/package_publish_target.py rename to frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.py index 34eee02562..ee6d5d8ca9 100644 --- a/frappe/custom/doctype/package_publish_target/package_publish_target.py +++ b/frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe from frappe.model.document import Document -class PackagePublishTarget(Document): +class EventUpdateLogConsumer(Document): pass diff --git a/frappe/exceptions.py b/frappe/exceptions.py index 88428b875c..267f5410af 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -76,6 +76,7 @@ class UnknownDomainError(Exception): pass class MappingMismatchError(ValidationError): pass class InvalidStatusError(ValidationError): pass class MandatoryError(ValidationError): pass +class NonNegativeError(ValidationError): pass class InvalidSignatureError(ValidationError): pass class RateLimitExceededError(ValidationError): pass class CannotChangeConstantError(ValidationError): pass @@ -109,3 +110,4 @@ class DocumentAlreadyRestored(Exception): pass class InvalidAuthorizationHeader(CSRFTokenError): pass class InvalidAuthorizationPrefix(CSRFTokenError): pass class InvalidAuthorizationToken(CSRFTokenError): pass +class InvalidDatabaseFile(ValidationError): pass \ No newline at end of file diff --git a/frappe/geo/doctype/currency/currency.json b/frappe/geo/doctype/currency/currency.json index bb9abb7ce8..db3fa5a19f 100644 --- a/frappe/geo/doctype/currency/currency.json +++ b/frappe/geo/doctype/currency/currency.json @@ -1,345 +1,113 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "field:currency_name", - "beta": 0, "creation": "2013-01-28 10:06:02", - "custom": 0, "description": "**Currency** Master", - "docstatus": 0, "doctype": "DocType", "document_type": "Setup", - "editable_grid": 0, + "engine": "InnoDB", + "field_order": [ + "currency_name", + "enabled", + "fraction", + "fraction_units", + "smallest_currency_fraction_value", + "symbol", + "number_format" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "currency_name", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Currency Name", - "length": 0, - "no_copy": 0, "oldfieldname": "currency_name", "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, "unique": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fieldname": "enabled", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, - "label": "Enabled", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Enabled" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "Sub-currency. For e.g. \"Cent\"", "fieldname": "fraction", "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": "Fraction", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Fraction" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "1 Currency = [?] Fraction\nFor e.g. 1 USD = 100 Cent", "fieldname": "fraction_units", "fieldtype": "Int", - "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": "Fraction Units", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Fraction Units" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "Smallest circulating fraction unit (coin). For e.g. 1 cent for USD and it should be entered as 0.01", "fieldname": "smallest_currency_fraction_value", "fieldtype": "Currency", - "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": "Smallest Currency Fraction 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, - "translatable": 0, - "unique": 0 + "non_negative": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "A symbol for this currency. For e.g. $", "fieldname": "symbol", "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": "Symbol", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Symbol" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "How should this currency be formatted? If not set, will use system defaults", "fieldname": "number_format", "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": "Number Format", - "length": 0, - "no_copy": 0, - "options": "\n#,###.##\n#.###,##\n# ###.##\n# ###,##\n#'###.##\n#, ###.##\n#,##,###.##\n#,###.###\n#.###\n#,###", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "\n#,###.##\n#.###,##\n# ###.##\n# ###,##\n#'###.##\n#, ###.##\n#,##,###.##\n#,###.###\n#.###\n#,###" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, "icon": "fa fa-bitcoin", "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-08-29 06:37:19.908254", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-10-29 06:33:12.879978", "modified_by": "Administrator", "module": "Geo", "name": "Currency", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, "import": 1, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, "read": 1, - "report": 0, - "role": "Accounts User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "role": "Accounts User" }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, "read": 1, - "report": 0, - "role": "Sales User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "role": "Sales User" }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, "read": 1, - "report": 0, - "role": "Purchase User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "role": "Purchase User" } ], - "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, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/installer.py b/frappe/installer.py index 27fca9088a..be9b04d453 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -3,7 +3,7 @@ import json import os - +from frappe.defaults import _clear_cache import frappe @@ -111,8 +111,8 @@ def remove_from_installed_apps(app_name): installed_apps = frappe.get_installed_apps() if app_name in installed_apps: installed_apps.remove(app_name) - frappe.db.set_global("installed_apps", json.dumps(installed_apps)) - frappe.get_single("Installed Applications").update_versions() + frappe.db.set_value("DefaultValue", {"defkey": "installed_apps"}, "defvalue", json.dumps(installed_apps)) + _clear_cache("__global") frappe.db.commit() if frappe.flags.in_install: post_install() @@ -122,64 +122,80 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False) """Remove app and all linked to the app's module with the app from a site.""" import click + site = frappe.local.site + # dont allow uninstall app if not installed unless forced if not force: if app_name not in frappe.get_installed_apps(): - click.secho("App {0} not installed on Site {1}".format(app_name, frappe.local.site), fg="yellow") + click.secho(f"App {app_name} not installed on Site {site}", fg="yellow") return - print("Uninstalling App {0} from Site {1}...".format(app_name, frappe.local.site)) + print(f"Uninstalling App {app_name} from Site {site}...") if not dry_run and not yes: - confirm = click.confirm("All doctypes (including custom), modules related to this app will be deleted. Are you sure you want to continue?") + confirm = click.confirm( + "All doctypes (including custom), modules related to this app will be" + " deleted. Are you sure you want to continue?" + ) if not confirm: return - if not no_backup: + if not (dry_run or no_backup): from frappe.utils.backups import scheduled_backup + print("Backing up...") scheduled_backup(ignore_files=True) frappe.flags.in_uninstall = True drop_doctypes = [] - modules = (x.name for x in frappe.get_all("Module Def", filters={"app_name": app_name})) + modules = frappe.get_all("Module Def", filters={"app_name": app_name}, pluck="name") for module_name in modules: - print("Deleting Module '{0}'".format(module_name)) + print(f"Deleting Module '{module_name}'") - for doctype in frappe.get_list("DocType", filters={"module": module_name}, fields=["name", "issingle"]): - print("* removing DocType '{0}'...".format(doctype.name)) + for doctype in frappe.get_all( + "DocType", filters={"module": module_name}, fields=["name", "issingle"] + ): + print(f"* removing DocType '{doctype.name}'...") if not dry_run: - frappe.delete_doc("DocType", doctype.name) + frappe.delete_doc("DocType", doctype.name, ignore_on_trash=True) if not doctype.issingle: drop_doctypes.append(doctype.name) - linked_doctypes = frappe.get_all("DocField", filters={"fieldtype": "Link", "options": "Module Def"}, fields=['parent']) + linked_doctypes = frappe.get_all( + "DocField", filters={"fieldtype": "Link", "options": "Module Def"}, fields=["parent"] + ) ordered_doctypes = ["Desk Page", "Report", "Page", "Web Form"] - doctypes_with_linked_modules = ordered_doctypes + [doctype.parent for doctype in linked_doctypes if doctype.parent not in ordered_doctypes] - + all_doctypes_with_linked_modules = ordered_doctypes + [ + doctype.parent + for doctype in linked_doctypes + if doctype.parent not in ordered_doctypes + ] + doctypes_with_linked_modules = [ + x for x in all_doctypes_with_linked_modules if frappe.db.exists("DocType", x) + ] for doctype in doctypes_with_linked_modules: - for record in frappe.get_list(doctype, filters={"module": module_name}): - print("* removing {0} '{1}'...".format(doctype, record.name)) + for record in frappe.get_all(doctype, filters={"module": module_name}, pluck="name"): + print(f"* removing {doctype} '{record}'...") if not dry_run: - frappe.delete_doc(doctype, record.name) + frappe.delete_doc(doctype, record, ignore_on_trash=True) - print("* removing Module Def '{0}'...".format(module_name)) + print(f"* removing Module Def '{module_name}'...") if not dry_run: - frappe.delete_doc("Module Def", module_name) + frappe.delete_doc("Module Def", module_name, ignore_on_trash=True) + + for doctype in set(drop_doctypes): + print(f"* dropping Table for '{doctype}'...") + if not dry_run: + frappe.db.sql_ddl(f"drop table `tab{doctype}`") if not dry_run: remove_from_installed_apps(app_name) - - for doctype in set(drop_doctypes): - print("* dropping Table for '{0}'...".format(doctype)) - frappe.db.sql_ddl("drop table `tab{0}`".format(doctype)) - frappe.db.commit() - click.secho("Uninstalled App {0} from Site {1}".format(app_name, frappe.local.site), fg="green") + click.secho(f"Uninstalled App {app_name} from Site {site}", fg="green") frappe.flags.in_uninstall = False @@ -406,3 +422,32 @@ def is_downgrade(sql_file_path, verbose=False): print("Your site will be downgraded from Frappe {0} to {1}".format(current_version, backup_version)) return downgrade + + +def validate_database_sql(path, _raise=True): + """Check if file has contents and if DefaultValue table exists + + Args: + path (str): Path of the decompressed SQL file + _raise (bool, optional): Raise exception if invalid file. Defaults to True. + """ + to_raise = False + error_message = "" + + if not os.path.getsize(path): + error_message = f"{path} is an empty file!" + to_raise = True + + if not _raise: + with open(path, "r") as f: + for line in f: + if 'tabDefaultValue' in line: + error_message = "Table `tabDefaultValue` not found in file." + to_raise = True + + if error_message: + import click + click.secho(error_message, fg="red") + + if _raise and to_raise: + raise frappe.InvalidDatabaseFile diff --git a/frappe/integrations/desk_page/integrations/integrations.json b/frappe/integrations/desk_page/integrations/integrations.json index cbf7c9c085..1acf4e6c4a 100644 --- a/frappe/integrations/desk_page/integrations/integrations.json +++ b/frappe/integrations/desk_page/integrations/integrations.json @@ -23,7 +23,7 @@ { "hidden": 0, "label": "Settings", - "links": "[\n {\n \"description\": \"Webhooks calling API requests into web apps\",\n \"label\": \"Webhook\",\n \"name\": \"Webhook\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Slack Webhooks for internal integration\",\n \"label\": \"Slack Webhook URL\",\n \"name\": \"Slack Webhook URL\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Twilio Settings for WhatsApp integration\",\n \"label\": \"Twilio Settings\",\n \"name\": \"Twilio Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"SMS Settings for sending sms\",\n \"label\": \"SMS Settings\",\n \"name\": \"SMS Settings\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"description\": \"Webhooks calling API requests into web apps\",\n \"label\": \"Webhook\",\n \"name\": \"Webhook\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Slack Webhooks for internal integration\",\n \"label\": \"Slack Webhook URL\",\n \"name\": \"Slack Webhook URL\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"SMS Settings for sending sms\",\n \"label\": \"SMS Settings\",\n \"name\": \"SMS Settings\",\n \"type\": \"doctype\"\n }\n]" } ], "category": "Administration", @@ -38,7 +38,7 @@ "idx": 0, "is_standard": 1, "label": "Integrations", - "modified": "2020-08-20 23:04:04.528572", + "modified": "2020-10-28 10:25:54.792363", "modified_by": "Administrator", "module": "Integrations", "name": "Integrations", diff --git a/frappe/integrations/doctype/twilio_number_group/__init__.py b/frappe/integrations/doctype/twilio_number_group/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/integrations/doctype/twilio_number_group/twilio_number_group.json b/frappe/integrations/doctype/twilio_number_group/twilio_number_group.json deleted file mode 100644 index 9d51e4b452..0000000000 --- a/frappe/integrations/doctype/twilio_number_group/twilio_number_group.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "actions": [], - "autoname": "field:phone_number", - "creation": "2020-02-24 13:58:58.036914", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "phone_number" - ], - "fields": [ - { - "fieldname": "phone_number", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Phone Number", - "options": "Phone", - "show_days": 1, - "show_seconds": 1, - "unique": 1 - } - ], - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2020-08-20 22:48:57.166791", - "modified_by": "Administrator", - "module": "Integrations", - "name": "Twilio Number Group", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/frappe/integrations/doctype/twilio_number_group/twilio_number_group.py b/frappe/integrations/doctype/twilio_number_group/twilio_number_group.py deleted file mode 100644 index 04cb9ae146..0000000000 --- a/frappe/integrations/doctype/twilio_number_group/twilio_number_group.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, 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 TwilioNumberGroup(Document): - pass diff --git a/frappe/integrations/doctype/twilio_settings/__init__.py b/frappe/integrations/doctype/twilio_settings/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/integrations/doctype/twilio_settings/test_twilio_settings.py b/frappe/integrations/doctype/twilio_settings/test_twilio_settings.py deleted file mode 100644 index bcb1368d68..0000000000 --- a/frappe/integrations/doctype/twilio_settings/test_twilio_settings.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies and Contributors -# See license.txt -from __future__ import unicode_literals - -# import frappe -import unittest - -class TestTwilioSettings(unittest.TestCase): - pass diff --git a/frappe/integrations/doctype/twilio_settings/twilio_settings.js b/frappe/integrations/doctype/twilio_settings/twilio_settings.js deleted file mode 100644 index 59ebcf2e7d..0000000000 --- a/frappe/integrations/doctype/twilio_settings/twilio_settings.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2020, Frappe Technologies and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Twilio Settings', { - refresh: function(frm) { - frm.dashboard.set_headline(__("For more information, {0}.", [`${__('Click here')}`])); - } -}); diff --git a/frappe/integrations/doctype/twilio_settings/twilio_settings.json b/frappe/integrations/doctype/twilio_settings/twilio_settings.json deleted file mode 100644 index 9eb2c0c512..0000000000 --- a/frappe/integrations/doctype/twilio_settings/twilio_settings.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "actions": [], - "creation": "2020-01-28 15:21:44.457163", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "enabled", - "account_sid", - "auth_token", - "column_break_2", - "twilio_number" - ], - "fields": [ - { - "fieldname": "account_sid", - "fieldtype": "Data", - "label": "Account SID", - "mandatory_depends_on": "eval: doc.enabled" - }, - { - "fieldname": "auth_token", - "fieldtype": "Password", - "label": "Auth Token", - "mandatory_depends_on": "eval: doc.enabled" - }, - { - "fieldname": "column_break_2", - "fieldtype": "Column Break" - }, - { - "fieldname": "twilio_number", - "fieldtype": "Table", - "label": "Twilio Number", - "options": "Twilio Number Group" - }, - { - "default": "0", - "fieldname": "enabled", - "fieldtype": "Check", - "label": "Enabled" - } - ], - "index_web_pages_for_search": 1, - "issingle": 1, - "links": [], - "modified": "2020-09-03 10:17:21.318743", - "modified_by": "Administrator", - "module": "Integrations", - "name": "Twilio Settings", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/frappe/integrations/doctype/twilio_settings/twilio_settings.py b/frappe/integrations/doctype/twilio_settings/twilio_settings.py deleted file mode 100644 index b8f991e829..0000000000 --- a/frappe/integrations/doctype/twilio_settings/twilio_settings.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe.model.document import Document -from frappe import _ -from frappe.utils.password import get_decrypted_password -from twilio.rest import Client -from six import string_types -from json import loads - -class TwilioSettings(Document): - def on_update(self): - if self.enabled: - self.validate_twilio_credentials() - - def validate_twilio_credentials(self): - try: - auth_token = get_decrypted_password("Twilio Settings", "Twilio Settings", 'auth_token') - client = Client(self.account_sid, auth_token) - client.api.accounts(self.account_sid).fetch() - except Exception: - frappe.throw(_("Invalid Account SID or Auth Token.")) - -def send_whatsapp_message(sender, receiver_list, message): - twilio_settings = frappe.get_doc("Twilio Settings") - if not twilio_settings.enabled: - frappe.throw(_("Please enable twilio settings before sending WhatsApp messages")) - - if isinstance(receiver_list, string_types): - receiver_list = loads(receiver_list) - if not isinstance(receiver_list, list): - receiver_list = [receiver_list] - - auth_token = get_decrypted_password("Twilio Settings", "Twilio Settings", 'auth_token') - client = Client(twilio_settings.account_sid, auth_token) - args = { - "from_": 'whatsapp:+{}'.format(sender), - "body": message - } - - failed_delivery = [] - - for rec in receiver_list: - args.update({"to": 'whatsapp:{}'.format(rec)}) - resp = _send_whatsapp(args, client) - if not resp or resp.error_message: - failed_delivery.append(rec) - - if failed_delivery: - frappe.log_error(_("The message wasn't correctly delivered to: {}".format(", ".join(failed_delivery))), _('Delivery Failed')) - - -def _send_whatsapp(message_dict, client): - response = frappe._dict() - try: - response = client.messages.create(**message_dict) - except Exception as e: - frappe.log_error(e, title = _('Twilio WhatsApp Message Error')) - - return response \ No newline at end of file diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index a38470e3f5..862abe375c 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -335,19 +335,25 @@ def clear_timeline_references(link_doctype, link_name): WHERE `tabCommunication Link`.link_doctype=%s AND `tabCommunication Link`.link_name=%s""", (link_doctype, link_name)) def insert_feed(doc): - from frappe.utils import get_fullname - - if frappe.flags.in_install or frappe.flags.in_import or getattr(doc, "no_feed_on_delete", False): + if ( + frappe.flags.in_install + or frappe.flags.in_uninstall + or frappe.flags.in_import + or getattr(doc, "no_feed_on_delete", False) + ): return + from frappe.utils import get_fullname + frappe.get_doc({ "doctype": "Comment", "comment_type": "Deleted", "reference_doctype": doc.doctype, "subject": "{0} {1}".format(_(doc.doctype), doc.name), - "full_name": get_fullname(doc.owner) + "full_name": get_fullname(doc.owner), }).insert(ignore_permissions=True) + def delete_controllers(doctype, module): """ Delete controller code in the doctype folder diff --git a/frappe/model/document.py b/frappe/model/document.py index 53fcd99f78..3789e20b19 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -493,6 +493,7 @@ class Document(BaseDocument): self._validate_mandatory() self._validate_data_fields() self._validate_selects() + self._validate_non_negative() self._validate_length() self._extract_images_from_text_editor() self._sanitize_content() @@ -503,6 +504,7 @@ class Document(BaseDocument): for d in children: d._validate_data_fields() d._validate_selects() + d._validate_non_negative() d._validate_length() d._extract_images_from_text_editor() d._sanitize_content() @@ -514,6 +516,21 @@ class Document(BaseDocument): else: self.validate_set_only_once() + def _validate_non_negative(self): + def get_msg(df): + if self.parentfield: + return "{} {} #{}: {} {}".format(frappe.bold(_(self.doctype)), + _("Row"), self.idx, _("Value cannot be negative for"), frappe.bold(_(df.label))) + else: + return _("Value cannot be negative for {0}: {1}").format(_(df.parent), frappe.bold(_(df.label))) + + for df in self.meta.get('fields', {'non_negative': ('=', 1), + 'fieldtype': ('in', ['Int', 'Float', 'Currency'])}): + + if flt(self.get(df.fieldname)) < 0: + msg = get_msg(df) + frappe.throw(msg, frappe.NonNegativeError, title=_("Negative Value")) + def validate_workflow(self): """Validate if the workflow transition is valid""" if frappe.flags.in_install == 'frappe': return diff --git a/frappe/patches.txt b/frappe/patches.txt index 2a6ba321d5..0daf29e001 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -314,3 +314,5 @@ execute:frappe.db.set_value('Website Settings', 'Website Settings', {'navbar_tem frappe.patches.v13_0.delete_event_producer_and_consumer_keys frappe.patches.v13_0.web_template_set_module #2020-10-05 frappe.patches.v13_0.remove_custom_link +execute:frappe.delete_doc("DocType", "Footer Item") +frappe.patches.v13_0.replace_field_target_with_open_in_new_tab diff --git a/frappe/patches/v13_0/replace_field_target_with_open_in_new_tab.py b/frappe/patches/v13_0/replace_field_target_with_open_in_new_tab.py new file mode 100644 index 0000000000..21b2d8ef03 --- /dev/null +++ b/frappe/patches/v13_0/replace_field_target_with_open_in_new_tab.py @@ -0,0 +1,11 @@ +import frappe + + +def execute(): + doctype = "Top Bar Item" + if not frappe.db.table_exists(doctype) \ + or not frappe.db.has_column(doctype, "target"): + return + + frappe.reload_doc("website", "doctype", "top_bar_item") + frappe.db.set_value(doctype, {"target": 'target = "_blank"'}, 'open_in_new_tab', 1) diff --git a/frappe/public/build.json b/frappe/public/build.json index 242cf0160a..a3622499d5 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -245,7 +245,9 @@ "public/js/frappe/ui/chart.js", "public/js/frappe/ui/datatable.js", "public/js/frappe/ui/driver.js", - "public/js/frappe/barcode_scanner/index.js" + "public/js/frappe/barcode_scanner/index.js", + + "public/js/frappe/widgets/utils.js" ], "css/form.min.css": [ "public/less/form_grid.less" diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js index f3f04ec4d8..2f051a4701 100644 --- a/frappe/public/js/frappe/form/controls/base_input.js +++ b/frappe/public/js/frappe/form/controls/base_input.js @@ -133,18 +133,6 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({ me.parse_validate_and_set_in_model(me.get_input_value(), e); }); }, - bind_focusout: function() { - // on touchscreen devices, scroll to top - // so that static navbar and page head don't overlap the input - if (frappe.dom.is_touchscreen()) { - var me = this; - this.$input && this.$input.on("focusout", function() { - if (frappe.dom.is_touchscreen()) { - frappe.utils.scroll_to(me.$wrapper); - } - }); - } - }, set_label: function(label) { if(label) this.df.label = label; diff --git a/frappe/public/js/frappe/form/controls/code.js b/frappe/public/js/frappe/form/controls/code.js index a547cfcf32..f3c51e0232 100644 --- a/frappe/public/js/frappe/form/controls/code.js +++ b/frappe/public/js/frappe/form/controls/code.js @@ -66,6 +66,7 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ const ace_language_mode = language_map[language] || ''; this.editor.session.setMode(ace_language_mode); + this.editor.setKeyboardHandler('ace/keyboard/vscode'); }, parse(value) { diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js index bbf9a89072..4db2553bd1 100644 --- a/frappe/public/js/frappe/form/controls/data.js +++ b/frappe/public/js/frappe/form/controls/data.js @@ -21,7 +21,6 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({ this.input = this.$input.get(0); this.has_input = true; this.bind_change_event(); - this.bind_focusout(); this.setup_autoname_check(); if (this.df.options == 'Phone') { this.setup_phone(); diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index 3b6ccd9a5c..b6daabc616 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -35,6 +35,7 @@ frappe.ui.form.Dashboard = Class.extend({ // clear custom this.wrapper.find('.custom').remove(); + this.hide(); }, set_headline: function(html, color) { this.frm.layout.show_message(html, color); @@ -171,7 +172,7 @@ frappe.ui.form.Dashboard = Class.extend({ if(this.data.graph) { this.setup_graph(); - show = true; + // show = true; } if(show) { @@ -494,6 +495,9 @@ frappe.ui.form.Dashboard = Class.extend({ callback: function(r) { if(r.message) { me.render_graph(r.message); + me.show(); + } else { + me.hide(); } } }); diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 2195568317..3505cf4857 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -113,7 +113,7 @@ frappe.ui.form.Layout = Class.extend({ label: __('Dashboard'), cssClass: 'form-dashboard', collapsible: 1, - //hidden: 1 + // hidden: 1 }); }, diff --git a/frappe/public/js/frappe/form/templates/form_sidebar.html b/frappe/public/js/frappe/form/templates/form_sidebar.html index 67e674b1c1..481d0159c3 100644 --- a/frappe/public/js/frappe/form/templates/form_sidebar.html +++ b/frappe/public/js/frappe/form/templates/form_sidebar.html @@ -60,7 +60,7 @@