From 93b9ca86f78423339ae5830eb81dba39c04a40b5 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Tue, 29 Oct 2019 15:02:10 +0530 Subject: [PATCH 001/226] perf: optimise globals search --- .../global_search_settings.py | 11 +++- frappe/utils/global_search.py | 53 +++++++++---------- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/frappe/desk/doctype/global_search_settings/global_search_settings.py b/frappe/desk/doctype/global_search_settings/global_search_settings.py index 0729fca5cb..42ac5db8d2 100644 --- a/frappe/desk/doctype/global_search_settings/global_search_settings.py +++ b/frappe/desk/doctype/global_search_settings/global_search_settings.py @@ -29,12 +29,21 @@ class GlobalSearchSettings(Document): repeated_dts = (", ".join([frappe.bold(dt) for dt in repeated_dts])) frappe.throw(_("Document Type {0} has been repeated.").format(repeated_dts)) + def on_update(self): + get_doctypes_for_global_search() + def get_doctypes_for_global_search(): doctypes = frappe.get_list("Global Search DocType", fields=["document_type"], order_by="idx ASC") if not doctypes: return [] - return [d.document_type for d in doctypes] + priorities = [d.document_type for d in doctypes] + allowed_doctypes = ",".join(["'{0}'".format(dt) for dt in priorities]) + + frappe.cache().hset("global_search", "search_priorities", priorities) + frappe.cache().hset("global_search", "allowed_doctypes", allowed_doctypes) + + return priorities, allowed_doctypes @frappe.whitelist() def reset_global_search_settings_doctypes(): diff --git a/frappe/utils/global_search.py b/frappe/utils/global_search.py index 447466770d..7c38298dc3 100644 --- a/frappe/utils/global_search.py +++ b/frappe/utils/global_search.py @@ -418,10 +418,19 @@ def search(text, start=0, limit=20, doctype=""): from frappe.desk.doctype.global_search_settings.global_search_settings import get_doctypes_for_global_search results = [] - texts = [t.strip() for t in text.split('&') if t] - priorities = get_doctypes_for_global_search() - allowed_doctypes = ",".join(["'{0}'".format(dt) for dt in priorities]) - for text in texts: + sorted_results = [] + + priorities = frappe.cache().hget("global_search", "search_priorities") + allowed_doctypes = frappe.cache().hget("global_search", "allowed_doctypes") + + if not priorities or not allowed_doctypes: + priorities, allowed_doctypes = get_doctypes_for_global_search() + + for text in set(text.split('&')): + text = text.strip() + if not text: + continue + mariadb_conditions = '' postgres_conditions = '' offset = '' @@ -455,36 +464,22 @@ def search(text, start=0, limit=20, doctype=""): 'postgres': common_query.format(fields=postgres_fields, conditions=postgres_conditions, limit=limit, offset=offset) }, as_dict=True) - tmp_result=[] - for i in result: - if i.rank > 0.0: - if i in results or not results: - tmp_result.extend([i]) - results.extend(tmp_result) - - for r in results: - try: - if frappe.get_meta(r.doctype).image_field: - r.image = frappe.db.get_value(r.doctype, r.name, frappe.get_meta(r.doctype).image_field) - except Exception: - frappe.clear_messages() - - sorted_results = [] + results.extend(result) for priority in priorities: - tmp_result = [] - if not results: - break - for index, r in enumerate(results): - if r.doctype == priority: - tmp_result.extend([r]) + if r.doctype == priority and r.rank > 0.0: + try: + meta = frappe.get_meta(r.doctype) + if meta.image_field: + r.image = frappe.db.get_value(r.doctype, r.name, meta.image_field) + except Exception: + frappe.clear_messages() + + sorted_results.extend([r]) results.pop(index) - sorted_results.extend(tmp_result) - - return sorted_results - + return sorted_results or results @frappe.whitelist(allow_guest=True) def web_search(text, scope=None, start=0, limit=20): From 369e4ff5782ef30d8e01506d4b3c58a1ec71d18f Mon Sep 17 00:00:00 2001 From: Himanshu Date: Tue, 29 Oct 2019 15:22:38 +0530 Subject: [PATCH 002/226] Patch(Tags): Check if column exists (#8682) * fix: tags patch * fix: auto_commit_on_many_writes * fix: check if tag exists * fix: check if tag or tag link exists * fix: check if column exists * fix: set autocommit false * fix: use ignore in insert query for bulk insert * fix: add option to ignore duplicates --- frappe/database/database.py | 5 +++-- frappe/patches.txt | 1 + frappe/patches/v12_0/setup_tags.py | 19 +++++++++++-------- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index 95096ed2d9..1e6a85236e 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -968,7 +968,7 @@ class Database(object): frappe.flags.touched_tables = set() frappe.flags.touched_tables.update(tables) - def bulk_insert(self, doctype, fields, values): + def bulk_insert(self, doctype, fields, values, ignore_duplicates=False): """ Insert multiple records at a time @@ -982,7 +982,8 @@ class Database(object): for idx, value in enumerate(values): insert_list.append(tuple(value)) if idx and (idx%10000 == 0 or idx < len(values)-1): - self.sql("""INSERT INTO `tab{doctype}` ({fields}) VALUES {values}""".format( + self.sql("""INSERT {ignore_duplicates} INTO `tab{doctype}` ({fields}) VALUES {values}""".format( + ignore_duplicates="IGNORE" if ignore_duplicates else "", doctype=doctype, fields=fields, values=", ".join(['%s'] * len(insert_list)) diff --git a/frappe/patches.txt b/frappe/patches.txt index 8321debf3a..621a2107df 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -254,3 +254,4 @@ frappe.patches.v12_0.delete_duplicate_indexes frappe.patches.v12_0.set_default_incoming_email_port frappe.patches.v12_0.update_global_search execute:frappe.reload_doc('desk', 'doctype', 'notification_settings') +frappe.patches.v12_0.setup_tags diff --git a/frappe/patches/v12_0/setup_tags.py b/frappe/patches/v12_0/setup_tags.py index cd50b0d505..d663cb2e0e 100644 --- a/frappe/patches/v12_0/setup_tags.py +++ b/frappe/patches/v12_0/setup_tags.py @@ -12,19 +12,22 @@ def execute(): time = frappe.utils.get_datetime() for doctype in frappe.get_list("DocType", filters={"istable": 0, "issingle": 0}): - for dt_tags in frappe.db.sql("select `name`, `_user_tags` from `tab{0}`".format(doctype.name), as_dict=True): - tags = dt_tags.get("_user_tags").split(",") if dt_tags.get("_user_tags") else None - if not tags: + if not frappe.db.count(doctype.name) or not frappe.db.has_column(doctype.name, "_user_tags"): + continue + + for _user_tags in frappe.db.sql("select `name`, `_user_tags` from `tab{0}`".format(doctype.name), as_dict=True): + if not _user_tags.get("_user_tags"): continue - for tag in tags: + for tag in _user_tags.get("_user_tags").split(",") if _user_tags.get("_user_tags") else []: if not tag: continue tag_list.append((tag.strip(), time, time, 'Administrator')) - tag_link_name = frappe.generate_hash(dt_tags.name + tag.strip() + doctype.name, 10), - tag_links.append((tag_link_name, doctype.name, dt_tags.name, tag.strip(), time, time, 'Administrator')) + tag_link_name = frappe.generate_hash(_user_tags.name + tag.strip() + doctype.name, 10) + tag_links.append((tag_link_name, doctype.name, _user_tags.name, tag.strip(), time, time, 'Administrator')) - frappe.db.bulk_insert("Tag", fields=["name", "creation", "modified", "modified_by"], values=set(tag_list)) - frappe.db.bulk_insert("Tag Link", fields=["name", "document_type", "document_name", "tag", "creation", "modified", "modified_by"], values=set(tag_links)) \ No newline at end of file + + frappe.db.bulk_insert("Tag", fields=["name", "creation", "modified", "modified_by"], values=set(tag_list), ignore_duplicates=True) + frappe.db.bulk_insert("Tag Link", fields=["name", "document_type", "document_name", "tag", "creation", "modified", "modified_by"], values=set(tag_links), ignore_duplicates=True) From f185ed979088bf4e0906f721bc033f06c30dd7b9 Mon Sep 17 00:00:00 2001 From: Sahil Khan Date: Wed, 30 Oct 2019 13:09:26 +0550 Subject: [PATCH 003/226] bumped to version 12.0.18 --- frappe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 6424dcd9b5..77207e017f 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -23,7 +23,7 @@ if sys.version[0] == '2': reload(sys) sys.setdefaultencoding("utf-8") -__version__ = '12.0.17' +__version__ = '12.0.18' __title__ = "Frappe Framework" local = Local() From 7cd329fac97ee02745345a5ccd6eb4ac21839342 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 24 Sep 2019 00:16:44 +0530 Subject: [PATCH 004/226] reactor(scheduler): created "Scheduler Job Type" and cleaned up scheduler --- .pylintrc | 2 + frappe/__init__.py | 16 +- frappe/core/doctype/communication/email.py | 13 +- frappe/core/doctype/doctype/doctype.json | 15 +- .../core/doctype/doctype_action/__init__.py | 0 .../doctype_action/doctype_action.json | 54 ++++ .../doctype/doctype_action/doctype_action.py | 10 + .../doctype/scheduled_job_log/__init__.py | 0 .../scheduled_job_log/scheduled_job_log.js | 8 + .../scheduled_job_log/scheduled_job_log.json | 62 +++++ .../scheduled_job_log/scheduled_job_log.py | 10 + .../test_scheduled_job_log.py | 10 + .../doctype/scheduled_job_type/__init__.py | 0 .../scheduled_job_type/scheduled_job_type.js | 8 + .../scheduled_job_type.json | 92 +++++++ .../scheduled_job_type/scheduled_job_type.py | 129 +++++++++ .../test_scheduled_job_type.py | 62 +++++ frappe/core/doctype/version/version.py | 2 +- .../doctype/email_account/email_account.py | 3 +- frappe/email/doctype/newsletter/newsletter.py | 3 +- frappe/email/queue.py | 3 +- frappe/email/receive.py | 5 +- frappe/hooks.py | 13 +- frappe/migrate.py | 3 + frappe/model/__init__.py | 2 +- frappe/model/base_document.py | 8 +- frappe/model/document.py | 4 +- frappe/model/meta.py | 11 +- frappe/model/sync.py | 1 + frappe/patches.txt | 1 + frappe/public/js/frappe/form/form.js | 19 ++ frappe/test_runner.py | 2 +- frappe/tests/test_scheduler.py | 68 +---- frappe/utils/background_jobs.py | 6 +- frappe/utils/scheduler.py | 249 ++---------------- 35 files changed, 561 insertions(+), 333 deletions(-) create mode 100644 .pylintrc create mode 100644 frappe/core/doctype/doctype_action/__init__.py create mode 100644 frappe/core/doctype/doctype_action/doctype_action.json create mode 100644 frappe/core/doctype/doctype_action/doctype_action.py create mode 100644 frappe/core/doctype/scheduled_job_log/__init__.py create mode 100644 frappe/core/doctype/scheduled_job_log/scheduled_job_log.js create mode 100644 frappe/core/doctype/scheduled_job_log/scheduled_job_log.json create mode 100644 frappe/core/doctype/scheduled_job_log/scheduled_job_log.py create mode 100644 frappe/core/doctype/scheduled_job_log/test_scheduled_job_log.py create mode 100644 frappe/core/doctype/scheduled_job_type/__init__.py create mode 100644 frappe/core/doctype/scheduled_job_type/scheduled_job_type.js create mode 100644 frappe/core/doctype/scheduled_job_type/scheduled_job_type.json create mode 100644 frappe/core/doctype/scheduled_job_type/scheduled_job_type.py create mode 100644 frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000000..c11c0ab6a3 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,2 @@ +disable=access-member-before-definition +disable=no-member \ No newline at end of file diff --git a/frappe/__init__.py b/frappe/__init__.py index 6424dcd9b5..5827541590 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -123,7 +123,6 @@ def init(site, sites_path=None, new_site=False): local.debug_log = [] local.realtime_log = [] local.flags = _dict({ - "ran_schedulers": [], "currently_saving": [], "redirect_location": "", "in_install_db": False, @@ -1504,7 +1503,20 @@ def logger(module=None, with_more_info=True): def log_error(message=None, title=None): '''Log error to Error Log''' - return get_doc(dict(doctype='Error Log', error=as_unicode(message or get_traceback()), + + # AI ALERT: + # the title and message may be swapped + # the better API for this is log_error(title, message), and used in many cases this way + # this hack tries to be smart about whats a title (single line ;-)) and fixes it + + if message: + if '\n' not in message: + title = message + error = get_traceback() + else: + error = message + + return get_doc(dict(doctype='Error Log', error=as_unicode(error), method=title)).insert(ignore_permissions=True) def get_desk_link(doctype, name): diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index a285941c68..361030d07a 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -10,7 +10,6 @@ from email.utils import formataddr from frappe.core.utils import get_parent_doc from frappe.utils import (get_url, get_formatted_email, cint, validate_email_address, split_emails, time_diff_in_seconds, parse_addr, get_datetime) -from frappe.utils.scheduler import log from frappe.email.email_body import get_message_id import frappe.email.smtp import time @@ -509,17 +508,7 @@ def sendmail(communication_name, print_html=None, print_format=None, attachments break except: - traceback = log("frappe.core.doctype.communication.email.sendmail", frappe.as_json({ - "communication_name": communication_name, - "print_html": print_html, - "print_format": print_format, - "attachments": attachments, - "recipients": recipients, - "cc": cc, - "bcc": bcc, - "lang": lang - })) - frappe.logger(__name__).error(traceback) + traceback = frappe.log_error("frappe.core.doctype.communication.email.sendmail") raise def update_mins_to_first_communication(parent, communication): diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index a6a5e258ee..834ac9b60b 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -57,6 +57,8 @@ "restrict_to_domain", "read_only", "in_create", + "actions_section", + "actions", "web_view", "has_web_view", "allow_guest_to_view", @@ -454,11 +456,22 @@ "fieldname": "nsm_parent_field", "fieldtype": "Data", "label": "Parent Field (Tree)" + }, + { + "fieldname": "actions_section", + "fieldtype": "Section Break", + "label": "Actions" + }, + { + "fieldname": "actions", + "fieldtype": "Table", + "label": "Actions", + "options": "DocType Action" } ], "icon": "fa fa-bolt", "idx": 6, - "modified": "2019-09-07 14:28:05.392490", + "modified": "2019-09-23 16:29:21.209832", "modified_by": "Administrator", "module": "Core", "name": "DocType", diff --git a/frappe/core/doctype/doctype_action/__init__.py b/frappe/core/doctype/doctype_action/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/doctype_action/doctype_action.json b/frappe/core/doctype/doctype_action/doctype_action.json new file mode 100644 index 0000000000..86757ea050 --- /dev/null +++ b/frappe/core/doctype/doctype_action/doctype_action.json @@ -0,0 +1,54 @@ +{ + "actions": [], + "creation": "2019-09-23 16:28:13.953520", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label", + "action_type", + "method", + "group" + ], + "fields": [ + { + "columns": 2, + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "reqd": 1 + }, + { + "columns": 6, + "fieldname": "method", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Method", + "reqd": 1 + }, + { + "fieldname": "group", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Group" + }, + { + "fieldname": "action_type", + "fieldtype": "Data", + "label": "Action Type", + "options": "Server Action" + } + ], + "istable": 1, + "modified": "2019-09-23 21:34:39.971700", + "modified_by": "Administrator", + "module": "Core", + "name": "DocType Action", + "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/core/doctype/doctype_action/doctype_action.py b/frappe/core/doctype/doctype_action/doctype_action.py new file mode 100644 index 0000000000..a745c7da40 --- /dev/null +++ b/frappe/core/doctype/doctype_action/doctype_action.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, 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 DocTypeAction(Document): + pass diff --git a/frappe/core/doctype/scheduled_job_log/__init__.py b/frappe/core/doctype/scheduled_job_log/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/scheduled_job_log/scheduled_job_log.js b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.js new file mode 100644 index 0000000000..d43160c658 --- /dev/null +++ b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Scheduled Job Log', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json new file mode 100644 index 0000000000..cfa2f27d1a --- /dev/null +++ b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json @@ -0,0 +1,62 @@ +{ + "creation": "2019-09-23 14:36:36.935869", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "status", + "scheduled_job", + "details" + ], + "fields": [ + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Scheduled\nSuccess\nFailed", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "scheduled_job", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Scheduled Job", + "options": "Scheduled Job Type", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "details", + "fieldtype": "Code", + "label": "Details", + "read_only": 1 + } + ], + "modified": "2019-09-23 14:36:36.935869", + "modified_by": "Administrator", + "module": "Core", + "name": "Scheduled Job Log", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "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/core/doctype/scheduled_job_log/scheduled_job_log.py b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.py new file mode 100644 index 0000000000..26871c9adf --- /dev/null +++ b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, 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 ScheduledJobLog(Document): + pass diff --git a/frappe/core/doctype/scheduled_job_log/test_scheduled_job_log.py b/frappe/core/doctype/scheduled_job_log/test_scheduled_job_log.py new file mode 100644 index 0000000000..1e5290425b --- /dev/null +++ b/frappe/core/doctype/scheduled_job_log/test_scheduled_job_log.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestScheduledJobLog(unittest.TestCase): + pass diff --git a/frappe/core/doctype/scheduled_job_type/__init__.py b/frappe/core/doctype/scheduled_job_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.js b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.js new file mode 100644 index 0000000000..55907b17fc --- /dev/null +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Scheduled Job Type', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json new file mode 100644 index 0000000000..c8dac630e9 --- /dev/null +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json @@ -0,0 +1,92 @@ +{ + "actions": [ + { + "action_path": "frappe.core.doctype.scheduled_job_type.scheduled_job_type.execute_event", + "label": "Execute", + "method": "frappe.core.doctype.scheduled_job_type.scheduled_job_type.execute_event" + } + ], + "creation": "2019-09-23 14:34:09.205368", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "stopped", + "method", + "queue", + "cron_format", + "last_execution", + "create_log" + ], + "fields": [ + { + "fieldname": "method", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Method", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "queue", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Queue", + "options": "All\nHourly\nDaily\nDaily Long\nWeekly\nWeekly Long\nMonthly\nMonthly Long\nCron\nYearly\nAnnual", + "read_only": 1, + "reqd": 1 + }, + { + "default": "0", + "fieldname": "stopped", + "fieldtype": "Check", + "label": "Stopped" + }, + { + "default": "0", + "depends_on": "eval:doc.queue==='All'", + "fieldname": "create_log", + "fieldtype": "Check", + "label": "Create Log" + }, + { + "fieldname": "last_execution", + "fieldtype": "Datetime", + "label": "Last Execution", + "read_only": 1 + }, + { + "allow_in_quick_entry": 1, + "depends_on": "eval:doc.queue==='Cron'", + "fieldname": "cron_format", + "fieldtype": "Data", + "label": "Cron Format", + "read_only": 1 + } + ], + "in_create": 1, + "modified": "2019-09-23 22:19:22.594874", + "modified_by": "Administrator", + "module": "Core", + "name": "Scheduled Job Type", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "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/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py new file mode 100644 index 0000000000..6733ddb014 --- /dev/null +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals + +import frappe, json +from frappe.model.document import Document +from frappe.utils import now_datetime, get_datetime +from datetime import datetime +from croniter import croniter +from frappe.utils.background_jobs import enqueue + +CRON_MAP = { + "Yearly": "0 0 1 1 *", + "Annual": "0 0 1 1 *", + "Monthly": "0 0 1 * *", + "Monthly Long": "0 0 1 * *", + "Weekly": "0 0 * * 0", + "Weekly Long": "0 0 * * 0", + "Daily": "0 0 * * *", + "Daily Long": "0 0 * * *", + "Hourly": "0 * * * *", + "Hourly Long": "0 * * * *", + "All": "0/" + str((frappe.get_conf().scheduler_interval or 240) // 60) + " * * * *", +} + +class ScheduledJobType(Document): + def autoname(self): + self.name = '.'.join(self.method.split('.')[-2:]) + + def validate(self): + if self.queue != 'All': + # force logging for all events other than continuous ones (ALL) + self.create_log = 1 + + def enqueue(self): + # enqueue event if last execution is done + if self.is_event_due(): + self.update_last_execution() + frappe.flags.enqueued_jobs.append(self.method) + enqueue('frappe.core.doctype.scheduled_job_type.scheduled_job_type.run_scheduled_job', + job_type=self.method) + + def is_event_due(self, current_time = None): + '''Return true if event is due based on time lapsed since last execution''' + # save last execution in expected execution time as per cron + self.last_execution = self.get_next_execution() + + # if the next scheduled event is before NOW, then its due! + return self.last_execution <= (current_time or now_datetime()) + + def get_next_execution(self): + if not self.cron_format: + self.cron_format = CRON_MAP[self.queue] + + return croniter(self.cron_format, + get_datetime(self.last_execution)).get_next(datetime) + + def execute(self): + try: + frappe.logger(__name__).info('Started Scheduled Job: {0} for {1}'.format(self.method, frappe.local.site)) + frappe.get_attr(self.method)() + frappe.db.commit() + frappe.logger(__name__).info('Completed Scheduled Job: {0} for {1}'.format(self.method, frappe.local.site)) + except Exception: + frappe.db.rollback() + frappe.log_error('{} failed'.format(self.method)) + frappe.logger(__name__).info('Failed Scheduled Job: {0} for {1}'.format(self.method, frappe.local.site)) + + + def update_last_execution(self): + self.db_set('last_execution', self.last_execution, update_modified=False) + frappe.db.commit() + + def get_queue_name(self): + return self.queue.replace(' ', '_').lower() + +@frappe.whitelist() +def execute_event(doc): + frappe.only_for('System Manager') + doc = json.loads(doc) + frappe.get_doc('Scheduled Job Type', doc.get('name')).execute() + +def run_scheduled_job(job_type): + '''This is a wrapper function that runs a hooks.scheduler_events method''' + frappe.get_doc('Scheduled Job Type', dict(method=job_type)).execute() + +def sync_jobs(): + frappe.reload_doc('core', 'doctype', 'scheduled_job_type') + all_events = [] + scheduler_events = frappe.get_hooks("scheduler_events") + insert_events(all_events, scheduler_events) + clear_events(all_events, scheduler_events) + +def insert_events(all_events, scheduler_events): + for event_type in scheduler_events: + events = scheduler_events.get(event_type) + if isinstance(events, dict): + insert_cron_event(events, all_events) + else: + # hourly, daily etc + insert_event_list(events, event_type, all_events) + +def insert_cron_event(events, all_events): + for cron_format in events: + for event in events.get(cron_format): + all_events.append(event) + insert_single_event('Cron', event, cron_format) + +def insert_event_list(events, event_type, all_events): + for event in events: + all_events.append(event) + queue = event_type.replace('_', ' ').title() + insert_single_event(queue, event) + +def insert_single_event(queue, event, cron_format = None): + if not frappe.db.exists('Scheduled Job Type', dict(method=event)): + frappe.get_doc(dict( + doctype = 'Scheduled Job Type', + method = event, + cron_format = cron_format, + queue = queue + )).insert() + +def clear_events(all_events, scheduler_events): + for event in frappe.get_all('Scheduled Job Type', ('name', 'method')): + if event.method not in all_events: + frappe.db.delete_doc('Scheduled Job Type', event.name) diff --git a/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py new file mode 100644 index 0000000000..75af876367 --- /dev/null +++ b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest +from frappe.utils import get_datetime + +from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs + +class TestScheduledJobType(unittest.TestCase): + def setUp(self): + if not frappe.get_all('Scheduled Job Type', limit=1): + frappe.db.rollback() + frappe.db.sql('truncate `tabScheduled Job Type`') + sync_jobs() + frappe.db.commit() + + def test_sync_jobs(self): + all_job = frappe.get_doc('Scheduled Job Type', + dict(method='frappe.email.queue.flush')) + self.assertEqual(all_job.queue, 'All') + + daily_job = frappe.get_doc('Scheduled Job Type', + dict(method='frappe.email.queue.clear_outbox')) + self.assertEqual(daily_job.queue, 'Daily') + + # check if cron jobs are synced + cron_job = frappe.get_doc('Scheduled Job Type', + dict(method='frappe.oauth.delete_oauth2_data')) + self.assertEqual(cron_job.queue, 'Cron') + self.assertEqual(cron_job.cron_format, '0/15 * * * *') + + def test_daily_job(self): + job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.email.queue.clear_outbox')) + job.db_set('last_execution', '2019-01-01 00:00:00') + self.assertTrue(job.is_event_due(get_datetime('2019-01-02 00:00:06'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-01 00:00:06'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-01 23:59:59'))) + + def test_weekly_job(self): + job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.utils.change_log.check_for_update')) + job.db_set('last_execution', '2019-01-01 00:00:00') + self.assertTrue(job.is_event_due(get_datetime('2019-01-06 00:00:01'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-02 00:00:06'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-05 23:59:59'))) + + def test_monthly_job(self): + job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.email.doctype.auto_email_report.auto_email_report.send_monthly')) + job.db_set('last_execution', '2019-01-01 00:00:00') + self.assertTrue(job.is_event_due(get_datetime('2019-02-01 00:00:01'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-15 00:00:06'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-31 23:59:59'))) + + def test_cron_job(self): + # runs every 15 mins + job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.oauth.delete_oauth2_data')) + job.db_set('last_execution', '2019-01-01 00:00:00') + self.assertTrue(job.is_event_due(get_datetime('2019-01-01 00:15:01'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-01 00:05:06'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-01 00:14:59'))) diff --git a/frappe/core/doctype/version/version.py b/frappe/core/doctype/version/version.py index 05cc102ab9..cb891afa0c 100644 --- a/frappe/core/doctype/version/version.py +++ b/frappe/core/doctype/version/version.py @@ -50,7 +50,7 @@ def get_diff(old, new, for_child=False): if df.fieldtype in no_value_fields and df.fieldtype not in table_fields: continue - old_value, new_value = old.get(df.fieldname), new.get(df.fieldname) + old_value, new_value = old.get(df.fieldname) or [], new.get(df.fieldname) or [] if df.fieldtype in table_fields: # make maps diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index f10f08664c..c05a0f3fe4 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -21,7 +21,6 @@ from frappe.desk.form import assign_to from frappe.utils.user import get_system_managers from frappe.utils.background_jobs import enqueue, get_jobs from frappe.core.doctype.communication.email import set_incoming_outgoing_accounts -from frappe.utils.scheduler import log from frappe.utils.html_utils import clean_email_html from frappe.email.utils import get_port @@ -284,7 +283,7 @@ class EmailAccount(Document): except Exception: frappe.db.rollback() - log('email_account.receive') + frappe.log_error('email_account.receive') if self.use_imap: self.handle_bad_emails(email_server, uid, msg, frappe.get_traceback()) exceptions.append(frappe.get_traceback()) diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index 4f3e7994a5..b0d1756643 100755 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -9,7 +9,6 @@ from frappe import throw, _ from frappe.website.website_generator import WebsiteGenerator from frappe.utils.verified_command import get_signed_params, verify_request from frappe.utils.background_jobs import enqueue -from frappe.utils.scheduler import log from frappe.email.queue import send from frappe.email.doctype.email_group.email_group import add_subscribers from frappe.utils import parse_addr @@ -213,7 +212,7 @@ def send_newsletter(newsletter): doc.db_set("email_sent", 0) frappe.db.commit() - log("send_newsletter") + frappe.log_error("send_newsletter") raise diff --git a/frappe/email/queue.py b/frappe/email/queue.py index f51451751d..792b47296a 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -12,7 +12,6 @@ from frappe.utils.verified_command import get_signed_params, verify_request from html2text import html2text from frappe.utils import get_url, nowdate, encode, now_datetime, add_days, split_emails, cstr, cint from rq.timeouts import JobTimeoutException -from frappe.utils.scheduler import log from six import text_type, string_types class EmailLimitCrossedError(frappe.ValidationError): pass @@ -469,7 +468,7 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals else: # log to Error Log - log('frappe.email.queue.flush', text_type(e)) + frappe.log_error('frappe.email.queue.flush') def prepare_message(email, recipient, recipients_list): message = email.message diff --git a/frappe/email/receive.py b/frappe/email/receive.py index ee7075b570..ac48b3e070 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -12,7 +12,6 @@ import frappe from frappe import _, safe_decode, safe_encode from frappe.utils import (extract_email_id, convert_utc_to_user_timezone, now, cint, cstr, strip, markdown, parse_addr) -from frappe.utils.scheduler import log from frappe.core.doctype.file.file import get_random_filename, MaxFileSizeReachedError class EmailSizeExceededError(frappe.ValidationError): pass @@ -80,7 +79,7 @@ class EmailServer: except _socket.error: # log performs rollback and logs error in Error Log - log("receive.connect_pop") + frappe.log_error("receive.connect_pop") # Invalid mail server -- due to refusing connection frappe.msgprint(_('Invalid Mail Server. Please rectify and try again.')) @@ -255,7 +254,7 @@ class EmailServer: else: # log performs rollback and logs error in Error Log - log("receive.get_messages", self.make_error_msg(msg_num, incoming_mail)) + frappe.log_error("receive.get_messages", self.make_error_msg(msg_num, incoming_mail)) self.errors = True frappe.db.rollback() diff --git a/frappe/hooks.py b/frappe/hooks.py index b35387efe9..5406d0fc57 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -76,8 +76,7 @@ leaderboards = "frappe.desk.leaderboard.get_leaderboards" on_session_creation = [ "frappe.core.doctype.activity_log.feed.login_feed", - "frappe.core.doctype.user.user.notify_admin_access_to_system_manager", - "frappe.utils.scheduler.reset_enabled_scheduler_events", + "frappe.core.doctype.user.user.notify_admin_access_to_system_manager" ] on_logout = "frappe.core.doctype.session_default_settings.session_default_settings.clear_session_defaults" @@ -153,14 +152,18 @@ doc_events = { } scheduler_events = { + "cron": { + "0/15 * * * *": [ + "frappe.oauth.delete_oauth2_data", + "frappe.website.doctype.web_page.web_page.check_publish_status", + "frappe.twofactor.delete_all_barcodes_for_users" + ] + }, "all": [ "frappe.email.queue.flush", "frappe.email.doctype.email_account.email_account.pull", "frappe.email.doctype.email_account.email_account.notify_unreplied", - "frappe.oauth.delete_oauth2_data", "frappe.integrations.doctype.razorpay_settings.razorpay_settings.capture_payment", - "frappe.twofactor.delete_all_barcodes_for_users", - "frappe.website.doctype.web_page.web_page.check_publish_status", 'frappe.utils.global_search.sync_global_search' ], "hourly": [ diff --git a/frappe/migrate.py b/frappe/migrate.py index 6778a3f18f..043b6817d7 100644 --- a/frappe/migrate.py +++ b/frappe/migrate.py @@ -15,6 +15,7 @@ from frappe.desk.notifications import clear_notifications from frappe.website import render from frappe.core.doctype.language.language import sync_languages from frappe.modules.utils import sync_customizations +from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs from frappe.utils import global_search def migrate(verbose=True, rebuild_website=False, skip_failing=False): @@ -46,9 +47,11 @@ def migrate(verbose=True, rebuild_website=False, skip_failing=False): # run patches frappe.modules.patch_handler.run_all(skip_failing) + # sync frappe.model.sync.sync_all(verbose=verbose) frappe.translate.clear_cache() + sync_jobs() sync_fixtures() sync_customizations() sync_languages() diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index 09db9bb68a..1c6f965d42 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -45,7 +45,7 @@ default_fields = ('doctype','name','owner','creation','modified','modified_by', 'parent','parentfield','parenttype','idx','docstatus') optional_fields = ("_user_tags", "_comments", "_assign", "_liked_by", "_seen") table_fields = ('Table', 'Table MultiSelect') -core_doctypes_list = ('DocType', 'DocField', 'DocPerm', 'User', 'Role', 'Has Role', +core_doctypes_list = ('DocType', 'DocField', 'DocPerm', 'DocType Action','User', 'Role', 'Has Role', 'Page', 'Module Def', 'Print Format', 'Report', 'Customize Form', 'Customize Form Field', 'Property Setter', 'Custom Field', 'Custom Script') diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 6c917b8d4d..1648c64a13 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -22,6 +22,8 @@ max_positive_value = { 'bigint': 2 ** 63 } +DOCTYPES_FOR_DOCTYPE = ('DocType', 'DocField', 'DocPerm', 'DocType Action') + _classes = {} def get_controller(doctype): @@ -255,7 +257,7 @@ class BaseDocument(object): def get_valid_columns(self): if self.doctype not in frappe.local.valid_columns: - if self.doctype in ("DocField", "DocPerm") and self.parent in ("DocType", "DocField", "DocPerm"): + if self.doctype in DOCTYPES_FOR_DOCTYPE: from frappe.model.meta import get_table_columns valid = get_table_columns(self.doctype) else: @@ -312,7 +314,7 @@ class BaseDocument(object): self.created_by = self.modified_by = frappe.session.user # if doctype is "DocType", don't insert null values as we don't know who is valid yet - d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in ('DocType', 'DocField', 'DocPerm')) + d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in DOCTYPES_FOR_DOCTYPE) columns = list(d) try: @@ -347,7 +349,7 @@ class BaseDocument(object): self.db_insert() return - d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in ('DocType', 'DocField', 'DocPerm')) + d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in DOCTYPES_FOR_DOCTYPE) # don't update name, as case might've been changed name = d['name'] diff --git a/frappe/model/document.py b/frappe/model/document.py index 7f04895308..c58e09ef5a 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -150,8 +150,8 @@ class Document(BaseDocument): super(Document, self).__init__(d) if self.name=="DocType" and self.doctype=="DocType": - from frappe.model.meta import doctype_table_fields - table_fields = doctype_table_fields + from frappe.model.meta import DOCTYPE_TABLE_FIELDS + table_fields = DOCTYPE_TABLE_FIELDS else: table_fields = self.meta.get_table_fields() diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 0b1011b119..cd342b77aa 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -151,7 +151,7 @@ class Meta(Document): if self.name!="DocType": self._table_fields = self.get('fields', {"fieldtype": ['in', table_fields]}) else: - self._table_fields = doctype_table_fields + self._table_fields = DOCTYPE_TABLE_FIELDS return self._table_fields @@ -165,7 +165,7 @@ class Meta(Document): def get_valid_columns(self): if not hasattr(self, "_valid_columns"): - if self.name in ("DocType", "DocField", "DocPerm", "Property Setter"): + if self.name in ("DocType", "DocField", "DocPerm", 'DocType Action',"Property Setter"): self._valid_columns = get_table_columns(self.name) else: self._valid_columns = self.default_fields + \ @@ -174,7 +174,7 @@ class Meta(Document): return self._valid_columns def get_table_field_doctype(self, fieldname): - return { "fields": "DocField", "permissions": "DocPerm"}.get(fieldname) + return { "fields": "DocField", "permissions": "DocPerm", "actions": "DocType Action"}.get(fieldname) def get_field(self, fieldname): '''Return docfield from meta''' @@ -441,9 +441,10 @@ class Meta(Document): def is_nested_set(self): return self.has_field('lft') and self.has_field('rgt') -doctype_table_fields = [ +DOCTYPE_TABLE_FIELDS = [ frappe._dict({"fieldname": "fields", "options": "DocField"}), - frappe._dict({"fieldname": "permissions", "options": "DocPerm"}) + frappe._dict({"fieldname": "permissions", "options": "DocPerm"}), + frappe._dict({"fieldname": "actions", "options": "DocType Action"}), ] ####### diff --git a/frappe/model/sync.py b/frappe/model/sync.py index 18bf827c5f..070ab6bbc7 100644 --- a/frappe/model/sync.py +++ b/frappe/model/sync.py @@ -29,6 +29,7 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe # these need to go first at time of install for d in (("core", "docfield"), ("core", "docperm"), + ("core", "doctype_action"), ("core", "role"), ("core", "has_role"), ("core", "doctype"), diff --git a/frappe/patches.txt b/frappe/patches.txt index e107449dea..1a89641a6a 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -9,6 +9,7 @@ frappe.patches.v7_2.remove_in_filter frappe.patches.v11_0.drop_column_apply_user_permissions execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2017-09-22 execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2018-02-20 +execute:frappe.reload_doc('core', 'doctype', 'doctype_action', force=True) #2019-09-23 execute:frappe.reload_doc('core', 'doctype', 'custom_docperm') execute:frappe.reload_doc('core', 'doctype', 'docperm') #2018-05-29 execute:frappe.reload_doc('core', 'doctype', 'comment') diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 5a983986d8..edf8860a01 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -111,6 +111,7 @@ frappe.ui.form.Form = class FrappeForm { $("body").attr("data-sidebar", 1); } this.setup_file_drop(); + this.setup_doctype_actions(); this.setup_done = true; } @@ -319,6 +320,24 @@ frappe.ui.form.Form = class FrappeForm { } } + // sets up the refresh event for custom buttons + // added via configuration + setup_doctype_actions() { + if (this.meta.actions) { + for (let action of this.meta.actions) { + frappe.ui.form.on(this.doctype, 'refresh', () => { + if (!this.is_new()) { + this.add_custom_button(action.label, () => { + frappe.xcall(action.method, {doc: this.doc}).then(() => { + frappe.msgprint({message:__('Event Executed'), alert:true}); + }); + }, action.group); + } + }); + } + } + } + switch_doc(docname) { // record switch if(this.docname != docname && (!this.meta.in_dialog || this.in_form) && !this.meta.istable) { diff --git a/frappe/test_runner.py b/frappe/test_runner.py index 76140e442c..d06d890ac9 100644 --- a/frappe/test_runner.py +++ b/frappe/test_runner.py @@ -71,7 +71,7 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(), else: ret = run_all_tests(app, verbose, profile, ui_tests, failfast=failfast) - frappe.db.commit() + if frappe.db: frappe.db.commit() # workaround! since there is no separate test db frappe.clear_cache() diff --git a/frappe/tests/test_scheduler.py b/frappe/tests/test_scheduler.py index 1f8c7943c4..4e193354b0 100644 --- a/frappe/tests/test_scheduler.py +++ b/frappe/tests/test_scheduler.py @@ -2,11 +2,9 @@ from __future__ import unicode_literals from unittest import TestCase from dateutil.relativedelta import relativedelta -from frappe.utils.scheduler import (enqueue_applicable_events, restrict_scheduler_events_if_dormant, - get_enabled_scheduler_events) -from frappe import _dict +from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs from frappe.utils.background_jobs import enqueue -from frappe.utils import now_datetime, today, add_days, add_to_date +from frappe.utils.scheduler import enqueue_events import frappe import time @@ -17,60 +15,19 @@ def test_timeout(): class TestScheduler(TestCase): def setUp(self): - frappe.db.set_global('enabled_scheduler_events', "") - frappe.flags.ran_schedulers = [] - - def test_all_events(self): - last = now_datetime() - relativedelta(hours=2) - enqueue_applicable_events(frappe.local.site, now_datetime(), last) - self.assertTrue("all" in frappe.flags.ran_schedulers) - - def test_enabled_events(self): - frappe.flags.enabled_events = ["hourly", "hourly_long", "daily", "daily_long", - "weekly", "weekly_long", "monthly", "monthly_long"] - - # maintain last_event and next_event on the same day - last_event = now_datetime().replace(hour=0, minute=0, second=0, microsecond=0) - next_event = last_event + relativedelta(minutes=30) - - enqueue_applicable_events(frappe.local.site, next_event, last_event) - self.assertFalse("cron" in frappe.flags.ran_schedulers) - - # maintain last_event and next_event on the same day - last_event = now_datetime().replace(hour=0, minute=0, second=0, microsecond=0) - next_event = last_event + relativedelta(hours=2) - - frappe.flags.ran_schedulers = [] - enqueue_applicable_events(frappe.local.site, next_event, last_event) - self.assertTrue("all" in frappe.flags.ran_schedulers) - self.assertTrue("hourly" in frappe.flags.ran_schedulers) - - frappe.flags.enabled_events = None - - def test_enabled_events_day_change(self): - - # use flags instead of globals as this test fails intermittently - # the root cause has not been identified but the culprit seems cache - # since cache is mutable, it maybe be changed by a parallel process - frappe.flags.enabled_events = ["daily", "daily_long", "weekly", "weekly_long", - "monthly", "monthly_long"] - - # maintain last_event and next_event on different days - next_event = now_datetime().replace(hour=0, minute=0, second=0, microsecond=0) - last_event = next_event - relativedelta(hours=2) - - frappe.flags.ran_schedulers = [] - enqueue_applicable_events(frappe.local.site, next_event, last_event) - self.assertTrue("all" in frappe.flags.ran_schedulers) - self.assertFalse("hourly" in frappe.flags.ran_schedulers) - - frappe.flags.enabled_events = None - - + if not frappe.get_all('Scheduled Job Type', limit=1): + sync_jobs() + def test_enqueue_jobs(self): + frappe.db.sql('update `tabScheduled Job Type` set last_execution = "2010-01-01 00:00:00"') + enqueue_events(site = frappe.local.site) + self.assertTrue('frappe.email.queue.clear_outbox', frappe.flags.enqueued_jobs) + self.assertTrue('frappe.utils.change_log.check_for_update', frappe.flags.enqueued_jobs) + self.assertTrue('frappe.email.doctype.auto_email_report.auto_email_report.send_monthly', frappe.flags.enqueued_jobs) def test_job_timeout(self): + return job = enqueue(test_timeout, timeout=10) count = 5 while count > 0: @@ -80,6 +37,3 @@ class TestScheduler(TestCase): break self.assertTrue(job.is_failed) - - def tearDown(self): - frappe.flags.ran_schedulers = [] diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py index ef345c67df..61e866e2f8 100755 --- a/frappe/utils/background_jobs.py +++ b/frappe/utils/background_jobs.py @@ -79,8 +79,6 @@ def run_doc_method(doctype, name, doc_method, **kwargs): def execute_job(site, method, event, job_name, kwargs, user=None, is_async=True, retry=0): '''Executes job in a worker, performs commit/rollback and logs if there is any error''' - from frappe.utils.scheduler import log - if is_async: frappe.connect(site) if os.environ.get('CI'): @@ -115,12 +113,12 @@ def execute_job(site, method, event, job_name, kwargs, user=None, is_async=True, is_async=is_async, retry=retry+1) else: - log(method_name, message=repr(locals())) + frappe.log_error(method_name) raise except: frappe.db.rollback() - log(method_name, message=repr(locals())) + frappe.log_error(method_name) raise else: diff --git a/frappe/utils/scheduler.py b/frappe/utils/scheduler.py index 68c3bc58a8..efd7fc6c12 100755 --- a/frappe/utils/scheduler.py +++ b/frappe/utils/scheduler.py @@ -10,38 +10,15 @@ Events: from __future__ import unicode_literals, print_function -import frappe -import json +import frappe, os, time import schedule -import time -import frappe.utils -import os +from frappe.utils import now_datetime, get_datetime from frappe.utils import get_sites -from datetime import datetime -from frappe.utils.background_jobs import enqueue, get_jobs, queue_timeout -from frappe.utils.data import get_datetime, now_datetime from frappe.core.doctype.user.user import STANDARD_USERS -from frappe.installer import update_site_config -from six import string_types -from croniter import croniter +from frappe.utils.background_jobs import get_jobs DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S' -cron_map = { - "yearly": "0 0 1 1 *", - "annual": "0 0 1 1 *", - "monthly": "0 0 1 * *", - "monthly_long": "0 0 1 * *", - "weekly": "0 0 * * 0", - "weekly_long": "0 0 * * 0", - "daily": "0 0 * * *", - "daily_long": "0 0 * * *", - "midnight": "0 0 * * *", - "hourly": "0 * * * *", - "hourly_long": "0 * * * *", - "all": "0/" + str((frappe.get_conf().scheduler_interval or 240) // 60) + " * * * *", -} - def start_scheduler(): '''Run enqueue_events_for_all_sites every 2 minutes (default). Specify scheduler_interval in seconds in common_site_config.json''' @@ -60,17 +37,16 @@ def enqueue_events_for_all_sites(): return with frappe.init_site(): - jobs_per_site = get_jobs() sites = get_sites() for site in sites: try: - enqueue_events_for_site(site=site, queued_jobs=jobs_per_site[site]) + enqueue_events_for_site(site=site) except: # it should try to enqueue other sites print(frappe.get_traceback()) -def enqueue_events_for_site(site, queued_jobs): +def enqueue_events_for_site(site): def log_and_raise(): frappe.logger(__name__).error('Exception in Enqueue Events for Site {0}'.format(site) + '\n' + frappe.get_traceback()) @@ -82,7 +58,7 @@ def enqueue_events_for_site(site, queued_jobs): if is_scheduler_inactive(): return - enqueue_events(site=site, queued_jobs=queued_jobs) + enqueue_events(site=site) frappe.logger(__name__).debug('Queued events for site {0}'.format(site)) except frappe.db.OperationalError as e: @@ -96,128 +72,13 @@ def enqueue_events_for_site(site, queued_jobs): finally: frappe.destroy() -def enqueue_events(site, queued_jobs): - nowtime = frappe.utils.now_datetime() - last = frappe.db.get_value('System Settings', 'System Settings', 'scheduler_last_event') - - # set scheduler last event - frappe.db.set_value('System Settings', 'System Settings', - 'scheduler_last_event', nowtime.strftime(DATETIME_FORMAT), - update_modified=False) - frappe.db.commit() - - out = [] - if last: - last = datetime.strptime(last, DATETIME_FORMAT) - out = enqueue_applicable_events(site, nowtime, last, queued_jobs) - - return '\n'.join(out) - -def enqueue_applicable_events(site, nowtime, last, queued_jobs=()): - nowtime_str = nowtime.strftime(DATETIME_FORMAT) - out = [] - - enabled_events = get_enabled_scheduler_events() - - def trigger_if_enabled(site, event, last, queued_jobs): - trigger(site, event, last, queued_jobs) - _log(event) - - def _log(event): - out.append("{time} - {event} - queued".format(time=nowtime_str, event=event)) - - for event in enabled_events: - trigger_if_enabled(site, event, last, queued_jobs) - - if "all" not in enabled_events: - trigger_if_enabled(site, "all", last, queued_jobs) - - return out - -def trigger(site, event, last=None, queued_jobs=(), now=False): - """Trigger method in hooks.scheduler_events.""" - - queue = 'long' if event.endswith('_long') else 'short' - timeout = queue_timeout[queue] - if not queued_jobs and not now: - queued_jobs = get_jobs(site=site, queue=queue) - - if frappe.flags.in_test: - frappe.flags.ran_schedulers.append(event) - - events_from_hooks = get_scheduler_events(event) - if not events_from_hooks: - return - - events = events_from_hooks - if not now: - events = [] - if event == "cron": - for e in events_from_hooks: - e = cron_map.get(e, e) - if croniter.is_valid(e): - if croniter(e, last).get_next(datetime) <= frappe.utils.now_datetime(): - events.extend(events_from_hooks[e]) - else: - frappe.log_error("Cron string " + e + " is not valid", "Error triggering cron job") - frappe.logger(__name__).error('Exception in Trigger Events for Site {0}, Cron String {1}'.format(site, e)) - - else: - if croniter(cron_map[event], last).get_next(datetime) <= frappe.utils.now_datetime(): - events.extend(events_from_hooks) - - for handler in events: - if not now: - if handler not in queued_jobs: - enqueue(handler, queue, timeout, event) - else: - scheduler_task(site=site, event=event, handler=handler, now=True) - -def get_scheduler_events(event): - '''Get scheduler events from hooks and integrations''' - scheduler_events = frappe.cache().get_value('scheduler_events') - if not scheduler_events: - scheduler_events = frappe.get_hooks("scheduler_events") - frappe.cache().set_value('scheduler_events', scheduler_events) - - return scheduler_events.get(event) or [] - -def log(method, message=None): - """log error in patch_log""" - message = frappe.utils.cstr(message) + "\n" if message else "" - message += frappe.get_traceback() - - if not (frappe.db and frappe.db._conn): - frappe.connect() - - frappe.db.rollback() - frappe.db.begin() - - d = frappe.new_doc("Error Log") - d.method = method - d.error = message - d.insert(ignore_permissions=True) - - frappe.db.commit() - - return message - -def get_enabled_scheduler_events(): - if 'enabled_events' in frappe.flags and frappe.flags.enabled_events: - return frappe.flags.enabled_events - - enabled_events = frappe.db.get_global("enabled_scheduler_events") - if frappe.flags.in_test: - # TEMP for debug: this test fails randomly - print('found enabled_scheduler_events {0}'.format(enabled_events)) - - if enabled_events: - if isinstance(enabled_events, string_types): - enabled_events = json.loads(enabled_events) - return enabled_events - - return ["all", "hourly", "hourly_long", "daily", "daily_long", - "weekly", "weekly_long", "monthly", "monthly_long", "cron"] +def enqueue_events(site): + frappe.flags.enqueued_jobs = [] + queued_jobs = get_jobs(key='job_type').get(site) or [] + for job_type in frappe.get_all('Scheduled Job Type', dict(stopped=0)): + if not job_type.method in queued_jobs: + # don't add it to queue if still pending + frappe.get_doc('Scheduled Job Type', job_type.name).enqueue() def is_scheduler_inactive(): if frappe.local.conf.maintenance_mode: @@ -229,6 +90,9 @@ def is_scheduler_inactive(): if is_scheduler_disabled(): return True + if is_dormant(): + return True + return False def is_scheduler_disabled(): @@ -246,90 +110,15 @@ def enable_scheduler(): def disable_scheduler(): toggle_scheduler(False) -def get_errors(from_date, to_date, limit): - errors = frappe.db.sql("""select modified, method, error from `tabError Log` - where date(modified) between %s and %s - and error not like '%%[Errno 110] Connection timed out%%' - order by modified limit %s""", (from_date, to_date, limit), as_dict=True) - return ["""

Time: {modified}

Method: {method}\n{error}
""".format(**e) - for e in errors] - -def get_error_report(from_date=None, to_date=None, limit=10): - from frappe.utils import get_url, now_datetime, add_days - - if not from_date: - from_date = add_days(now_datetime().date(), -1) - if not to_date: - to_date = add_days(now_datetime().date(), -1) - - errors = get_errors(from_date, to_date, limit) - - if errors: - return 1, """

Error Logs (max {limit}):

-

URL: {url}


{errors}""".format( - limit=limit, url=get_url(), errors="
".join(errors)) - else: - return 0, "

No error logs

" - -def scheduler_task(site, event, handler, now=False): - '''This is a wrapper function that runs a hooks.scheduler_events method''' - frappe.logger(__name__).info('running {handler} for {site} for event: {event}'.format(handler=handler, site=site, event=event)) - try: - if not now: - frappe.connect(site=site) - - frappe.flags.in_scheduler = True - frappe.get_attr(handler)() - - except Exception: - frappe.db.rollback() - traceback = log(handler, "Method: {event}, Handler: {handler}".format(event=event, handler=handler)) - frappe.logger(__name__).error(traceback) - raise - - else: - frappe.db.commit() - - frappe.logger(__name__).info('ran {handler} for {site} for event: {event}'.format(handler=handler, site=site, event=event)) - - -def reset_enabled_scheduler_events(login_manager): - if login_manager.info.user_type == "System User": - try: - if frappe.db.get_global('enabled_scheduler_events'): - # clear restricted events, someone logged in! - frappe.db.set_global('enabled_scheduler_events', None) - except frappe.db.InternalError as e: - if frappe.db.is_timedout(e): - frappe.log_error(frappe.get_traceback(), "Error in reset_enabled_scheduler_events") - else: - raise - else: - is_dormant = frappe.conf.get('dormant') - if is_dormant: - update_site_config('dormant', 'None') - - -def restrict_scheduler_events_if_dormant(): - if is_dormant(): - restrict_scheduler_events() - update_site_config('dormant', True) - -def restrict_scheduler_events(*args, **kwargs): - val = json.dumps(["hourly", "hourly_long", "daily", "daily_long", "weekly", "weekly_long", "monthly", "monthly_long", "cron"]) - frappe.db.set_global('enabled_scheduler_events', val) - def is_dormant(since = 345600): last_user_activity = get_last_active() if not last_user_activity: # no user has ever logged in, so not yet used return False - last_active = get_datetime(last_user_activity) - # Get now without tz info - now = now_datetime().replace(tzinfo=None) - time_since_last_active = now - last_active - if time_since_last_active.total_seconds() > since: # 4 days + + if now_datetime() - get_datetime(last_user_activity) > since: # 4 days return True + return False def get_last_active(): From 34b4b069ba44985a8244c59ec08276ba41ed82aa Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 24 Sep 2019 10:11:03 +0530 Subject: [PATCH 005/226] feat(scheduler): log scheduler events --- .../doctype_action/doctype_action.json | 27 +++++----- .../scheduled_job_type.json | 4 +- .../scheduled_job_type/scheduled_job_type.py | 25 +++++++-- .../system_settings/system_settings.json | 19 +++---- frappe/database/mariadb/framework_mariadb.sql | 24 +++++++++ .../database/postgres/framework_postgres.sql | 25 +++++++++ frappe/public/js/frappe/form/form.js | 11 ++-- frappe/public/js/frappe/utils/utils.js | 6 +-- frappe/utils/scheduler.py | 54 +++++++++++-------- 9 files changed, 141 insertions(+), 54 deletions(-) diff --git a/frappe/core/doctype/doctype_action/doctype_action.json b/frappe/core/doctype/doctype_action/doctype_action.json index 86757ea050..7a1b845af3 100644 --- a/frappe/core/doctype/doctype_action/doctype_action.json +++ b/frappe/core/doctype/doctype_action/doctype_action.json @@ -7,7 +7,7 @@ "field_order": [ "label", "action_type", - "method", + "action", "group" ], "fields": [ @@ -19,14 +19,6 @@ "label": "Label", "reqd": 1 }, - { - "columns": 6, - "fieldname": "method", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Method", - "reqd": 1 - }, { "fieldname": "group", "fieldtype": "Data", @@ -34,14 +26,25 @@ "label": "Group" }, { + "columns": 2, "fieldname": "action_type", - "fieldtype": "Data", + "fieldtype": "Select", + "in_list_view": 1, "label": "Action Type", - "options": "Server Action" + "options": "Server Action", + "reqd": 1 + }, + { + "columns": 4, + "fieldname": "action", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Action", + "reqd": 1 } ], "istable": 1, - "modified": "2019-09-23 21:34:39.971700", + "modified": "2019-09-24 09:11:39.860100", "modified_by": "Administrator", "module": "Core", "name": "DocType Action", diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json index c8dac630e9..aea8611b6a 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json @@ -1,7 +1,9 @@ { "actions": [ { + "action": "frappe.core.doctype.scheduled_job_type.scheduled_job_type.execute_event", "action_path": "frappe.core.doctype.scheduled_job_type.scheduled_job_type.execute_event", + "action_type": "Server Action", "label": "Execute", "method": "frappe.core.doctype.scheduled_job_type.scheduled_job_type.execute_event" } @@ -66,7 +68,7 @@ } ], "in_create": 1, - "modified": "2019-09-23 22:19:22.594874", + "modified": "2019-09-24 09:11:29.115423", "modified_by": "Administrator", "module": "Core", "name": "Scheduled Job Type", diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py index 6733ddb014..5ebbf29a5f 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -39,6 +39,9 @@ class ScheduledJobType(Document): if self.is_event_due(): self.update_last_execution() frappe.flags.enqueued_jobs.append(self.method) + if frappe.flags.in_test: + self.execute() + else: enqueue('frappe.core.doctype.scheduled_job_type.scheduled_job_type.run_scheduled_job', job_type=self.method) @@ -58,16 +61,30 @@ class ScheduledJobType(Document): get_datetime(self.last_execution)).get_next(datetime) def execute(self): + self.scheduler_log = None try: - frappe.logger(__name__).info('Started Scheduled Job: {0} for {1}'.format(self.method, frappe.local.site)) + self.log_status('Start') frappe.get_attr(self.method)() frappe.db.commit() - frappe.logger(__name__).info('Completed Scheduled Job: {0} for {1}'.format(self.method, frappe.local.site)) + self.log_status('Complete') except Exception: frappe.db.rollback() - frappe.log_error('{} failed'.format(self.method)) - frappe.logger(__name__).info('Failed Scheduled Job: {0} for {1}'.format(self.method, frappe.local.site)) + self.log_status('Failed') + def log_status(self, status): + # log file + frappe.logger(__name__).info('Scheduled Job {0}: {1} for {2}'.format(status, self.method, frappe.local.site)) + self.update_scheduler_log(status) + + def update_scheduler_log(self, status): + if not self.create_log: + return + if not self.scheduler_log: + self.scheduler_log = frappe.get_doc(dict(doctype = 'Scheduled Job Log', scheduled_job=self.name)).insert(ignore_permissions=True) + self.scheduler_log.db_set('status', status) + if status == 'Failed': + self.scheduler_log.db_set('details', frappe.get_traceback()) + frappe.db.commit() def update_last_execution(self): self.db_set('last_execution', self.last_execution, update_modified=False) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 55bc5d49db..9e8bc8c3fd 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2014-04-17 16:53:52.640856", "doctype": "DocType", "document_type": "System", @@ -21,7 +22,7 @@ "backup_limit", "background_workers", "enable_scheduler", - "scheduler_last_event", + "dormant_days", "permissions", "apply_strict_user_permissions", "column_break_21", @@ -168,13 +169,6 @@ "hidden": 1, "label": "Enable Scheduled Jobs" }, - { - "fieldname": "scheduler_last_event", - "fieldtype": "Data", - "hidden": 1, - "label": "Scheduler Last Event", - "report_hide": 1 - }, { "collapsible": 1, "fieldname": "permissions", @@ -397,11 +391,18 @@ "fieldname": "allow_guests_to_upload_files", "fieldtype": "Check", "label": "Allow Guests to Upload Files" + }, + { + "default": "4", + "description": "Will run scheduled jobs only once a day for inactive sites. Default 4 days if set to 0.", + "fieldname": "dormant_days", + "fieldtype": "Int", + "label": "Run Jobs only Daily if Inactive For (Days)" } ], "icon": "fa fa-cog", "issingle": 1, - "modified": "2019-08-16 08:26:45.936626", + "modified": "2019-09-24 10:04:28.807388", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql index 7058ed0325..cc90938bae 100644 --- a/frappe/database/mariadb/framework_mariadb.sql +++ b/frappe/database/mariadb/framework_mariadb.sql @@ -105,6 +105,30 @@ CREATE TABLE `tabDocPerm` ( KEY `parent` (`parent`) ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +-- +-- Table structure for table `tabDocType Action` +-- + +| tabDocType Action | CREATE TABLE `tabDocType Action` ( + `name` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL, + `creation` datetime(6) DEFAULT NULL, + `modified` datetime(6) DEFAULT NULL, + `modified_by` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `owner` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `docstatus` int(1) NOT NULL DEFAULT 0, + `parent` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `parentfield` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `parenttype` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `idx` int(8) NOT NULL DEFAULT 0, + `label` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `group` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `action_type` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `action` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`name`), + KEY `parent` (`parent`), + KEY `modified` (`modified`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED | + -- -- Table structure for table `tabDocType` -- diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql index df59de92df..a0293b29f3 100644 --- a/frappe/database/postgres/framework_postgres.sql +++ b/frappe/database/postgres/framework_postgres.sql @@ -106,6 +106,31 @@ CREATE TABLE "tabDocPerm" ( create index on "tabDocPerm" ("parent"); +-- +-- Table structure for table "tabDocType Action" +-- + +DROP TABLE IF EXISTS "tabDocType Action"; +CREATE TABLE "tabDocType Action" ( + "name" varchar(255) NOT NULL, + "creation" timestamp(6) DEFAULT NULL, + "modified" timestamp(6) DEFAULT NULL, + "modified_by" varchar(255) DEFAULT NULL, + "owner" varchar(255) DEFAULT NULL, + "docstatus" smallint NOT NULL DEFAULT 0, + "parent" varchar(255) DEFAULT NULL, + "parentfield" varchar(255) DEFAULT NULL, + "parenttype" varchar(255) DEFAULT NULL, + "idx" bigint NOT NULL DEFAULT 0, + "label" varchar(140) DEFAULT NOT NULL, + "group" varchar(140) DEFAULT NULL, + "action_type" varchar(140) DEFAULT NOT NULL, + "action" varchar(140) DEFAULT NOT NULL, + PRIMARY KEY ("name") +) ; + +create index on "tabDocType Action" ("parent"); + -- -- Table structure for table "tabDocType" -- diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index edf8860a01..0c28b22f35 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -328,9 +328,14 @@ frappe.ui.form.Form = class FrappeForm { frappe.ui.form.on(this.doctype, 'refresh', () => { if (!this.is_new()) { this.add_custom_button(action.label, () => { - frappe.xcall(action.method, {doc: this.doc}).then(() => { - frappe.msgprint({message:__('Event Executed'), alert:true}); - }); + if (action.action_type==='Server Action') { + frappe.xcall(action.action, {doc: this.doc}).then(() => { + frappe.msgprint({ + message: __('{} Complete', [action.label]), + alert: true + }); + }); + } }, action.group); } }); diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 00b6f95f06..d4caa45a89 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -264,11 +264,11 @@ Object.assign(frappe.utils, { if(has_words(["Pending", "Review", "Medium", "Not Approved"], text)) { style = "warning"; colour = "orange"; - } else if(has_words(["Open", "Urgent", "High"], text)) { + } else if(has_words(["Open", "Urgent", "High", "Failed"], text)) { style = "danger"; colour = "red"; - } else if(has_words(["Closed", "Finished", "Converted", "Completed", "Confirmed", - "Approved", "Yes", "Active", "Available", "Paid"], text)) { + } else if(has_words(["Closed", "Finished", "Converted", "Completed", "Complete", "Confirmed", + "Approved", "Yes", "Active", "Available", "Paid", "Success"], text)) { style = "success"; colour = "green"; } else if(has_words(["Submitted"], text)) { diff --git a/frappe/utils/scheduler.py b/frappe/utils/scheduler.py index efd7fc6c12..afc2bec009 100755 --- a/frappe/utils/scheduler.py +++ b/frappe/utils/scheduler.py @@ -73,12 +73,13 @@ def enqueue_events_for_site(site): frappe.destroy() def enqueue_events(site): - frappe.flags.enqueued_jobs = [] - queued_jobs = get_jobs(key='job_type').get(site) or [] - for job_type in frappe.get_all('Scheduled Job Type', dict(stopped=0)): - if not job_type.method in queued_jobs: - # don't add it to queue if still pending - frappe.get_doc('Scheduled Job Type', job_type.name).enqueue() + if schedule_jobs_based_on_activity(): + frappe.flags.enqueued_jobs = [] + queued_jobs = get_jobs(key='job_type').get(site) or [] + for job_type in frappe.get_all('Scheduled Job Type', dict(stopped=0)): + if not job_type.method in queued_jobs: + # don't add it to queue if still pending + frappe.get_doc('Scheduled Job Type', job_type.name).enqueue() def is_scheduler_inactive(): if frappe.local.conf.maintenance_mode: @@ -90,9 +91,6 @@ def is_scheduler_inactive(): if is_scheduler_disabled(): return True - if is_dormant(): - return True - return False def is_scheduler_disabled(): @@ -110,19 +108,31 @@ def enable_scheduler(): def disable_scheduler(): toggle_scheduler(False) -def is_dormant(since = 345600): - last_user_activity = get_last_active() - if not last_user_activity: - # no user has ever logged in, so not yet used - return False - - if now_datetime() - get_datetime(last_user_activity) > since: # 4 days +def schedule_jobs_based_on_activity(): + '''Returns True for active sites defined by Activity Log + Returns True for inactive sites once in 24 hours''' + if is_dormant(): + # ensure last job is one day old + last_job = frappe.db.get_all('Scheduled Job Log', ('creation'), limit=1, order_by='creation desc') + if not last_job: + return True + else: + if (now_datetime() - get_datetime(last_job[0].creation)).seconds > 86400: + # one day is passed since jobs are run, so lets do this + return True + else: + # schedulers run in the last 24 hours, do nothing + return False + else: + # site active, lets run the jobs return True - return False +def is_dormant(): + last_activity_log = frappe.db.get_all('Activity Log', ('modified'), limit=1, order_by='modified desc') + since = (frappe.get_system_settings('dormant_days') or 4) * 86400 + if not last_activity_log: + return True + if (now_datetime() - get_datetime(last_activity_log[0].modified)).seconds > since: + return True -def get_last_active(): - return frappe.db.sql("""SELECT MAX(`last_active`) FROM `tabUser` - WHERE `user_type` = 'System User' AND `name` NOT IN ({standard_users})""" - .format(standard_users=", ".join(["%s"]*len(STANDARD_USERS))), - STANDARD_USERS)[0][0] + return False \ No newline at end of file From 3e4e6d4b3f48b86551d208cbc3ee84bbfd05679b Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 24 Sep 2019 12:50:51 +0530 Subject: [PATCH 006/226] feat(doctype link): add links to doctype for link dashboard --- frappe/core/doctype/doctype/doctype.json | 16 ++++++- frappe/core/doctype/doctype_link/__init__.py | 0 .../doctype/doctype_link/doctype_link.json | 46 +++++++++++++++++++ .../core/doctype/doctype_link/doctype_link.py | 10 ++++ .../scheduled_job_type.json | 12 +++-- .../scheduled_job_type/scheduled_job_type.py | 28 +++++------ frappe/database/mariadb/framework_mariadb.sql | 27 ++++++++++- .../database/postgres/framework_postgres.sql | 26 +++++++++++ frappe/installer.py | 2 + frappe/model/__init__.py | 2 +- frappe/model/base_document.py | 2 +- frappe/model/meta.py | 43 ++++++++++++++++- frappe/model/sync.py | 1 + frappe/patches.txt | 1 + 14 files changed, 191 insertions(+), 25 deletions(-) create mode 100644 frappe/core/doctype/doctype_link/__init__.py create mode 100644 frappe/core/doctype/doctype_link/doctype_link.json create mode 100644 frappe/core/doctype/doctype_link/doctype_link.py diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index 834ac9b60b..5c3fd8d302 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_rename": 1, "autoname": "Prompt", "creation": "2013-02-18 13:36:19", @@ -59,6 +60,8 @@ "in_create", "actions_section", "actions", + "links_section", + "links", "web_view", "has_web_view", "allow_guest_to_view", @@ -467,11 +470,22 @@ "fieldtype": "Table", "label": "Actions", "options": "DocType Action" + }, + { + "fieldname": "links_section", + "fieldtype": "Section Break", + "label": "Links Section" + }, + { + "fieldname": "links", + "fieldtype": "Table", + "label": "Links", + "options": "DocType Link" } ], "icon": "fa fa-bolt", "idx": 6, - "modified": "2019-09-23 16:29:21.209832", + "modified": "2019-09-24 11:42:18.081499", "modified_by": "Administrator", "module": "Core", "name": "DocType", diff --git a/frappe/core/doctype/doctype_link/__init__.py b/frappe/core/doctype/doctype_link/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/doctype_link/doctype_link.json b/frappe/core/doctype/doctype_link/doctype_link.json new file mode 100644 index 0000000000..752b4bb5da --- /dev/null +++ b/frappe/core/doctype/doctype_link/doctype_link.json @@ -0,0 +1,46 @@ +{ + "actions": [], + "creation": "2019-09-24 11:41:25.291377", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "link_doctype", + "link_fieldname", + "group" + ], + "fields": [ + { + "fieldname": "link_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Link DocType", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "link_fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Link Fieldname", + "reqd": 1 + }, + { + "fieldname": "group", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Group" + } + ], + "istable": 1, + "modified": "2019-09-24 11:41:25.291377", + "modified_by": "Administrator", + "module": "Core", + "name": "DocType Link", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/doctype_link/doctype_link.py b/frappe/core/doctype/doctype_link/doctype_link.py new file mode 100644 index 0000000000..efe8b09809 --- /dev/null +++ b/frappe/core/doctype/doctype_link/doctype_link.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, 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 DocTypeLink(Document): + pass diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json index aea8611b6a..ec68544ab5 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json @@ -2,10 +2,8 @@ "actions": [ { "action": "frappe.core.doctype.scheduled_job_type.scheduled_job_type.execute_event", - "action_path": "frappe.core.doctype.scheduled_job_type.scheduled_job_type.execute_event", "action_type": "Server Action", - "label": "Execute", - "method": "frappe.core.doctype.scheduled_job_type.scheduled_job_type.execute_event" + "label": "Execute" } ], "creation": "2019-09-23 14:34:09.205368", @@ -68,7 +66,13 @@ } ], "in_create": 1, - "modified": "2019-09-24 09:11:29.115423", + "links": [ + { + "link_doctype": "Scheduled Job Log", + "link_fieldname": "scheduled_job" + } + ], + "modified": "2019-09-24 11:45:34.748779", "modified_by": "Administrator", "module": "Core", "name": "Scheduled Job Type", diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py index 5ebbf29a5f..517f57469b 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -11,20 +11,6 @@ from datetime import datetime from croniter import croniter from frappe.utils.background_jobs import enqueue -CRON_MAP = { - "Yearly": "0 0 1 1 *", - "Annual": "0 0 1 1 *", - "Monthly": "0 0 1 * *", - "Monthly Long": "0 0 1 * *", - "Weekly": "0 0 * * 0", - "Weekly Long": "0 0 * * 0", - "Daily": "0 0 * * *", - "Daily Long": "0 0 * * *", - "Hourly": "0 * * * *", - "Hourly Long": "0 * * * *", - "All": "0/" + str((frappe.get_conf().scheduler_interval or 240) // 60) + " * * * *", -} - class ScheduledJobType(Document): def autoname(self): self.name = '.'.join(self.method.split('.')[-2:]) @@ -54,6 +40,20 @@ class ScheduledJobType(Document): return self.last_execution <= (current_time or now_datetime()) def get_next_execution(self): + CRON_MAP = { + "Yearly": "0 0 1 1 *", + "Annual": "0 0 1 1 *", + "Monthly": "0 0 1 * *", + "Monthly Long": "0 0 1 * *", + "Weekly": "0 0 * * 0", + "Weekly Long": "0 0 * * 0", + "Daily": "0 0 * * *", + "Daily Long": "0 0 * * *", + "Hourly": "0 * * * *", + "Hourly Long": "0 * * * *", + "All": "0/" + str((frappe.get_conf().scheduler_interval or 240) // 60) + " * * * *", + } + if not self.cron_format: self.cron_format = CRON_MAP[self.queue] diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql index cc90938bae..b1a769b189 100644 --- a/frappe/database/mariadb/framework_mariadb.sql +++ b/frappe/database/mariadb/framework_mariadb.sql @@ -109,7 +109,7 @@ CREATE TABLE `tabDocPerm` ( -- Table structure for table `tabDocType Action` -- -| tabDocType Action | CREATE TABLE `tabDocType Action` ( +CREATE TABLE `tabDocType Action` ( `name` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL, `creation` datetime(6) DEFAULT NULL, `modified` datetime(6) DEFAULT NULL, @@ -127,7 +127,30 @@ CREATE TABLE `tabDocPerm` ( PRIMARY KEY (`name`), KEY `parent` (`parent`), KEY `modified` (`modified`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED; + +-- +-- Table structure for table `tabDocType Action` +-- + +CREATE TABLE `tabDocType Link` ( + `name` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL, + `creation` datetime(6) DEFAULT NULL, + `modified` datetime(6) DEFAULT NULL, + `modified_by` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `owner` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `docstatus` int(1) NOT NULL DEFAULT 0, + `parent` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `parentfield` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `parenttype` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `idx` int(8) NOT NULL DEFAULT 0, + `group` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `link_doctype` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `link_fieldname` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`name`), + KEY `parent` (`parent`), + KEY `modified` (`modified`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED; -- -- Table structure for table `tabDocType` diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql index a0293b29f3..e49b461518 100644 --- a/frappe/database/postgres/framework_postgres.sql +++ b/frappe/database/postgres/framework_postgres.sql @@ -131,6 +131,32 @@ CREATE TABLE "tabDocType Action" ( create index on "tabDocType Action" ("parent"); +-- +-- Table structure for table "tabDocType Link" +-- + +DROP TABLE IF EXISTS "tabDocType Link"; +CREATE TABLE "tabDocType Link" ( + "name" varchar(255) NOT NULL, + "creation" timestamp(6) DEFAULT NULL, + "modified" timestamp(6) DEFAULT NULL, + "modified_by" varchar(255) DEFAULT NULL, + "owner" varchar(255) DEFAULT NULL, + "docstatus" smallint NOT NULL DEFAULT 0, + "parent" varchar(255) DEFAULT NULL, + "parentfield" varchar(255) DEFAULT NULL, + "parenttype" varchar(255) DEFAULT NULL, + "idx" bigint NOT NULL DEFAULT 0, + "label" varchar(140) DEFAULT NOT NULL, + "group" varchar(140) DEFAULT NULL, + "link_doctype" varchar(140) DEFAULT NOT NULL, + "link_fieldname" varchar(140) DEFAULT NOT NULL, + PRIMARY KEY ("name") +) ; + +create index on "tabDocType Link" ("parent"); + + -- -- Table structure for table "tabDocType" -- diff --git a/frappe/installer.py b/frappe/installer.py index 764a0b6780..f691a6cb22 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -18,6 +18,7 @@ from frappe.utils.fixtures import sync_fixtures from frappe.website import render from frappe.modules.utils import sync_customizations from frappe.database import setup_database +from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs def install_db(root_login="root", root_password=None, db_name=None, source_sql=None, admin_password=None, verbose=True, force=0, site_config=None, reinstall=False, @@ -91,6 +92,7 @@ def install_app(name, verbose=False, set_as_patched=True): for after_install in app_hooks.after_install or []: frappe.get_attr(after_install)() + sync_jobs() sync_fixtures(name) sync_customizations(name) diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index 1c6f965d42..1fe92d7a67 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -45,7 +45,7 @@ default_fields = ('doctype','name','owner','creation','modified','modified_by', 'parent','parentfield','parenttype','idx','docstatus') optional_fields = ("_user_tags", "_comments", "_assign", "_liked_by", "_seen") table_fields = ('Table', 'Table MultiSelect') -core_doctypes_list = ('DocType', 'DocField', 'DocPerm', 'DocType Action','User', 'Role', 'Has Role', +core_doctypes_list = ('DocType', 'DocField', 'DocPerm', 'DocType Action', 'DocType Link', 'User', 'Role', 'Has Role', 'Page', 'Module Def', 'Print Format', 'Report', 'Customize Form', 'Customize Form Field', 'Property Setter', 'Custom Field', 'Custom Script') diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 1648c64a13..a50bf9fdaf 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -22,7 +22,7 @@ max_positive_value = { 'bigint': 2 ** 63 } -DOCTYPES_FOR_DOCTYPE = ('DocType', 'DocField', 'DocPerm', 'DocType Action') +DOCTYPES_FOR_DOCTYPE = ('DocType', 'DocField', 'DocPerm', 'DocType Action', 'DocType Link') _classes = {} diff --git a/frappe/model/meta.py b/frappe/model/meta.py index cd342b77aa..97e526c4e0 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -165,7 +165,7 @@ class Meta(Document): def get_valid_columns(self): if not hasattr(self, "_valid_columns"): - if self.name in ("DocType", "DocField", "DocPerm", 'DocType Action',"Property Setter"): + if self.name in ("DocType", "DocField", "DocPerm", 'DocType Action', 'DocType Link', "Property Setter"): self._valid_columns = get_table_columns(self.name) else: self._valid_columns = self.default_fields + \ @@ -174,7 +174,12 @@ class Meta(Document): return self._valid_columns def get_table_field_doctype(self, fieldname): - return { "fields": "DocField", "permissions": "DocPerm", "actions": "DocType Action"}.get(fieldname) + return { + "fields": "DocField", + "permissions": "DocPerm", + "actions": "DocType Action", + 'links': 'DocType Link' + }.get(fieldname) def get_field(self, fieldname): '''Return docfield from meta''' @@ -419,11 +424,44 @@ class Meta(Document): except ImportError: pass + self.add_doctype_links(data) + for hook in frappe.get_hooks("override_doctype_dashboards", {}).get(self.name, []): data = frappe.get_attr(hook)(data=data) return data + def add_doctype_links(self, data): + '''add `links` child table in standard link dashboard format''' + if self.links: + if not data.transactions: + # init groups + data.transactions = [] + data.non_standard_fieldnames = {} + + for link in self.links: + link.added = False + for group in data.transactions: + # group found + if group.label == link.label: + if not link.link_doctype in group.items: + group.items.append(link.link_doctype) + link.added = True + + if not link.added: + # group not found, make a new group + data.transactions.append(dict( + label = link.group, + items = [link.link_doctype] + )) + + if link.link_fieldname != data.fieldname: + if data.fieldname: + data.non_standard_fieldnames[link.link_doctype] = link.link_fieldname + else: + data.fieldname = link.link_fieldname + + def get_row_template(self): return self.get_web_template(suffix='_row') @@ -445,6 +483,7 @@ DOCTYPE_TABLE_FIELDS = [ frappe._dict({"fieldname": "fields", "options": "DocField"}), frappe._dict({"fieldname": "permissions", "options": "DocPerm"}), frappe._dict({"fieldname": "actions", "options": "DocType Action"}), + frappe._dict({"fieldname": "links", "options": "DocType Link"}), ] ####### diff --git a/frappe/model/sync.py b/frappe/model/sync.py index 070ab6bbc7..2d9a48390d 100644 --- a/frappe/model/sync.py +++ b/frappe/model/sync.py @@ -30,6 +30,7 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe for d in (("core", "docfield"), ("core", "docperm"), ("core", "doctype_action"), + ("core", "doctype_link"), ("core", "role"), ("core", "has_role"), ("core", "doctype"), diff --git a/frappe/patches.txt b/frappe/patches.txt index 1a89641a6a..7d7a3dc2ea 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -10,6 +10,7 @@ frappe.patches.v11_0.drop_column_apply_user_permissions execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2017-09-22 execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2018-02-20 execute:frappe.reload_doc('core', 'doctype', 'doctype_action', force=True) #2019-09-23 +execute:frappe.reload_doc('core', 'doctype', 'doctype_link', force=True) #2019-09-23 execute:frappe.reload_doc('core', 'doctype', 'custom_docperm') execute:frappe.reload_doc('core', 'doctype', 'docperm') #2018-05-29 execute:frappe.reload_doc('core', 'doctype', 'comment') From 0035772f8fb7b7cd72af7bbde6b1099299554cb4 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 24 Sep 2019 21:45:48 +0530 Subject: [PATCH 007/226] fix(tests) --- frappe/core/doctype/version/version.py | 2 +- frappe/database/postgres/framework_postgres.sql | 12 ++++++------ .../email/doctype/notification/test_notification.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/frappe/core/doctype/version/version.py b/frappe/core/doctype/version/version.py index cb891afa0c..05cc102ab9 100644 --- a/frappe/core/doctype/version/version.py +++ b/frappe/core/doctype/version/version.py @@ -50,7 +50,7 @@ def get_diff(old, new, for_child=False): if df.fieldtype in no_value_fields and df.fieldtype not in table_fields: continue - old_value, new_value = old.get(df.fieldname) or [], new.get(df.fieldname) or [] + old_value, new_value = old.get(df.fieldname), new.get(df.fieldname) if df.fieldtype in table_fields: # make maps diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql index e49b461518..373a55279b 100644 --- a/frappe/database/postgres/framework_postgres.sql +++ b/frappe/database/postgres/framework_postgres.sql @@ -122,10 +122,10 @@ CREATE TABLE "tabDocType Action" ( "parentfield" varchar(255) DEFAULT NULL, "parenttype" varchar(255) DEFAULT NULL, "idx" bigint NOT NULL DEFAULT 0, - "label" varchar(140) DEFAULT NOT NULL, + "label" varchar(140) NOT NULL, "group" varchar(140) DEFAULT NULL, - "action_type" varchar(140) DEFAULT NOT NULL, - "action" varchar(140) DEFAULT NOT NULL, + "action_type" varchar(140) NOT NULL, + "action" varchar(140) NOT NULL, PRIMARY KEY ("name") ) ; @@ -147,10 +147,10 @@ CREATE TABLE "tabDocType Link" ( "parentfield" varchar(255) DEFAULT NULL, "parenttype" varchar(255) DEFAULT NULL, "idx" bigint NOT NULL DEFAULT 0, - "label" varchar(140) DEFAULT NOT NULL, + "label" varchar(140) NOT NULL, "group" varchar(140) DEFAULT NULL, - "link_doctype" varchar(140) DEFAULT NOT NULL, - "link_fieldname" varchar(140) DEFAULT NOT NULL, + "link_doctype" varchar(140) NOT NULL, + "link_fieldname" varchar(140) NOT NULL, PRIMARY KEY ("name") ) ; diff --git a/frappe/email/doctype/notification/test_notification.py b/frappe/email/doctype/notification/test_notification.py index 4d3c3167a5..b9bbde172d 100644 --- a/frappe/email/doctype/notification/test_notification.py +++ b/frappe/email/doctype/notification/test_notification.py @@ -136,7 +136,7 @@ class TestNotification(unittest.TestCase): "reference_name": event.name, "status": "Not Sent"})) frappe.set_user('Administrator') - frappe.utils.scheduler.trigger(frappe.local.site, "daily", now=True) + frappe.get_doc('Scheduled Job Type', dict(method='frappe.email.doctype.notification.notification.trigger_daily_alerts')).execute() # not today, so no alert self.assertFalse(frappe.db.get_value("Email Queue", {"reference_doctype": "Event", @@ -150,7 +150,7 @@ class TestNotification(unittest.TestCase): self.assertFalse(frappe.db.get_value("Email Queue", {"reference_doctype": "Event", "reference_name": event.name, "status": "Not Sent"})) - frappe.utils.scheduler.trigger(frappe.local.site, "daily", now=True) + frappe.get_doc('Scheduled Job Type', dict(method='frappe.email.doctype.notification.notification.trigger_daily_alerts')).execute() # today so show alert self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": "Event", From c583be6f33f4af37bac9e0ed0730b0e4e8afab86 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 24 Sep 2019 22:44:08 +0530 Subject: [PATCH 008/226] fix(scheduler): implement queue peeking --- .../scheduled_job_type/scheduled_job_type.py | 28 +++++++++++++------ .../database/postgres/framework_postgres.sql | 2 +- frappe/tests/test_scheduler.py | 21 +++++++++++++- frappe/utils/background_jobs.py | 15 +++++++--- 4 files changed, 51 insertions(+), 15 deletions(-) diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py index 517f57469b..609cfd06a1 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -9,7 +9,7 @@ from frappe.model.document import Document from frappe.utils import now_datetime, get_datetime from datetime import datetime from croniter import croniter -from frappe.utils.background_jobs import enqueue +from frappe.utils.background_jobs import enqueue, get_jobs class ScheduledJobType(Document): def autoname(self): @@ -24,12 +24,18 @@ class ScheduledJobType(Document): # enqueue event if last execution is done if self.is_event_due(): self.update_last_execution() - frappe.flags.enqueued_jobs.append(self.method) - if frappe.flags.in_test: - self.execute() - else: - enqueue('frappe.core.doctype.scheduled_job_type.scheduled_job_type.run_scheduled_job', - job_type=self.method) + if frappe.flags.enqueued_jobs: + frappe.flags.enqueued_jobs.append(self.method) + + if frappe.flags.execute_job: + self.execute() + else: + if not self.is_job_in_queue(): + enqueue('frappe.core.doctype.scheduled_job_type.scheduled_job_type.run_scheduled_job', + queue = self.get_queue_name(), job_type=self.method) + return True + else: + return False def is_event_due(self, current_time = None): '''Return true if event is due based on time lapsed since last execution''' @@ -39,6 +45,10 @@ class ScheduledJobType(Document): # if the next scheduled event is before NOW, then its due! return self.last_execution <= (current_time or now_datetime()) + def is_job_in_queue(self): + queued_jobs = get_jobs(site=frappe.local.site, key='job_type')[frappe.local.site] + return self.method in queued_jobs + def get_next_execution(self): CRON_MAP = { "Yearly": "0 0 1 1 *", @@ -91,7 +101,7 @@ class ScheduledJobType(Document): frappe.db.commit() def get_queue_name(self): - return self.queue.replace(' ', '_').lower() + return 'long' if ('Long' in self.queue) else 'default' @frappe.whitelist() def execute_event(doc): @@ -143,4 +153,4 @@ def insert_single_event(queue, event, cron_format = None): def clear_events(all_events, scheduler_events): for event in frappe.get_all('Scheduled Job Type', ('name', 'method')): if event.method not in all_events: - frappe.db.delete_doc('Scheduled Job Type', event.name) + frappe.delete_doc('Scheduled Job Type', event.name) diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql index 373a55279b..cd2f02d8e4 100644 --- a/frappe/database/postgres/framework_postgres.sql +++ b/frappe/database/postgres/framework_postgres.sql @@ -147,7 +147,7 @@ CREATE TABLE "tabDocType Link" ( "parentfield" varchar(255) DEFAULT NULL, "parenttype" varchar(255) DEFAULT NULL, "idx" bigint NOT NULL DEFAULT 0, - "label" varchar(140) NOT NULL, + "label" varchar(140) DEFAULT NULL, "group" varchar(140) DEFAULT NULL, "link_doctype" varchar(140) NOT NULL, "link_fieldname" varchar(140) NOT NULL, diff --git a/frappe/tests/test_scheduler.py b/frappe/tests/test_scheduler.py index 4e193354b0..c1b03f7251 100644 --- a/frappe/tests/test_scheduler.py +++ b/frappe/tests/test_scheduler.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from unittest import TestCase from dateutil.relativedelta import relativedelta from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs -from frappe.utils.background_jobs import enqueue +from frappe.utils.background_jobs import enqueue, get_jobs from frappe.utils.scheduler import enqueue_events import frappe @@ -20,12 +20,31 @@ class TestScheduler(TestCase): def test_enqueue_jobs(self): frappe.db.sql('update `tabScheduled Job Type` set last_execution = "2010-01-01 00:00:00"') + + frappe.flags.execute_job = True enqueue_events(site = frappe.local.site) + frappe.flags.execute_job = False self.assertTrue('frappe.email.queue.clear_outbox', frappe.flags.enqueued_jobs) self.assertTrue('frappe.utils.change_log.check_for_update', frappe.flags.enqueued_jobs) self.assertTrue('frappe.email.doctype.auto_email_report.auto_email_report.send_monthly', frappe.flags.enqueued_jobs) + def test_queue_peeking(self): + if not frappe.db.exists('Scheduled Job Type', 'test_scheduler.test_timeout'): + job = frappe.get_doc(dict( + doctype = 'Scheduled Job Type', + method = 'frappe.tests.test_scheduler.test_timeout', + last_execution = '2010-01-01 00:00:00', + queue = 'All' + )).insert() + else: + job = frappe.get_doc('Scheduled Job Type', 'test_scheduler.test_timeout') + + self.assertTrue(job.enqueue()) + print(get_jobs(site=frappe.local.site, key='job_type')) + self.assertFalse(job.enqueue()) + job.delete() + def test_job_timeout(self): return job = enqueue(test_timeout, timeout=10) diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py index 61e866e2f8..7681e76646 100755 --- a/frappe/utils/background_jobs.py +++ b/frappe/utils/background_jobs.py @@ -160,18 +160,25 @@ def get_worker_name(queue): def get_jobs(site=None, queue=None, key='method'): '''Gets jobs per queue or per site or both''' jobs_per_site = defaultdict(list) + + def add_to_dict(job): + if key in job.kwargs: + jobs_per_site[job.kwargs['site']].append(job.kwargs[key]) + + elif key in job.kwargs.get('kwargs', {}): + # optional keyword arguments are stored in 'kwargs' of 'kwargs' + jobs_per_site[job.kwargs['site']].append(job.kwargs['kwargs'][key]) + for queue in get_queue_list(queue): q = get_queue(queue) for job in q.jobs: if job.kwargs.get('site'): if site is None: - # get jobs for all sites - jobs_per_site[job.kwargs['site']].append(job.kwargs[key]) + add_to_dict(job) elif job.kwargs['site'] == site: - # get jobs only for given site - jobs_per_site[site].append(job.kwargs[key]) + add_to_dict(job) else: print('No site found in job', job.__dict__) From e85cd6549caa4d391b902739d0d56a79fda71f89 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 25 Sep 2019 09:55:17 +0530 Subject: [PATCH 009/226] fix(tests): fix queue peeking test --- frappe/__init__.py | 2 ++ frappe/core/doctype/scheduled_job_type/scheduled_job_type.py | 4 ++-- frappe/tests/test_scheduler.py | 5 ++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 5827541590..1ebaebce97 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1515,6 +1515,8 @@ def log_error(message=None, title=None): error = get_traceback() else: error = message + else: + error = get_traceback() return get_doc(dict(doctype='Error Log', error=as_unicode(error), method=title)).insert(ignore_permissions=True) diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py index 609cfd06a1..c08afa179a 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -34,8 +34,8 @@ class ScheduledJobType(Document): enqueue('frappe.core.doctype.scheduled_job_type.scheduled_job_type.run_scheduled_job', queue = self.get_queue_name(), job_type=self.method) return True - else: - return False + + return False def is_event_due(self, current_time = None): '''Return true if event is due based on time lapsed since last execution''' diff --git a/frappe/tests/test_scheduler.py b/frappe/tests/test_scheduler.py index c1b03f7251..b8c46d376f 100644 --- a/frappe/tests/test_scheduler.py +++ b/frappe/tests/test_scheduler.py @@ -37,11 +37,14 @@ class TestScheduler(TestCase): last_execution = '2010-01-01 00:00:00', queue = 'All' )).insert() + frappe.db.commit() else: job = frappe.get_doc('Scheduled Job Type', 'test_scheduler.test_timeout') self.assertTrue(job.enqueue()) - print(get_jobs(site=frappe.local.site, key='job_type')) + job.db_set('last_execution', '2010-01-01 00:00:00') + frappe.db.commit() + time.sleep(1) # wait if job is not yet queued self.assertFalse(job.enqueue()) job.delete() From d6f14608f104d6f64386c3b3d2f2b167285d1112 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 25 Sep 2019 11:06:22 +0530 Subject: [PATCH 010/226] fix(minor): set execute function in a try catch block to detect if execution cannot even begin --- frappe/core/doctype/scheduled_job_type/scheduled_job_type.py | 5 ++++- frappe/utils/scheduler.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py index c08afa179a..7c6b2a0e0c 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -111,7 +111,10 @@ def execute_event(doc): def run_scheduled_job(job_type): '''This is a wrapper function that runs a hooks.scheduler_events method''' - frappe.get_doc('Scheduled Job Type', dict(method=job_type)).execute() + try: + frappe.get_doc('Scheduled Job Type', dict(method=job_type)).execute() + except Exception: + print(frappe.get_traceback()) def sync_jobs(): frappe.reload_doc('core', 'doctype', 'scheduled_job_type') diff --git a/frappe/utils/scheduler.py b/frappe/utils/scheduler.py index afc2bec009..4d9f782542 100755 --- a/frappe/utils/scheduler.py +++ b/frappe/utils/scheduler.py @@ -75,8 +75,8 @@ def enqueue_events_for_site(site): def enqueue_events(site): if schedule_jobs_based_on_activity(): frappe.flags.enqueued_jobs = [] - queued_jobs = get_jobs(key='job_type').get(site) or [] - for job_type in frappe.get_all('Scheduled Job Type', dict(stopped=0)): + queued_jobs = get_jobs(site=site, key='job_type').get(site) or [] + for job_type in frappe.get_all('Scheduled Job Type', ('name', 'method'), dict(stopped=0)): if not job_type.method in queued_jobs: # don't add it to queue if still pending frappe.get_doc('Scheduled Job Type', job_type.name).enqueue() From fea34e3f6beba9507f5ca74ca45d2c94ddd7fdf1 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 25 Sep 2019 12:24:29 +0530 Subject: [PATCH 011/226] fix(tests): test_scheduler --- .../scheduled_job_log/scheduled_job_log.json | 20 +++--- .../scheduled_job_type.json | 4 +- .../scheduled_job_type/scheduled_job_type.py | 7 +- frappe/database/database.py | 12 +++- frappe/tests/test_scheduler.py | 65 +++++++++++++++---- frappe/utils/scheduler.py | 18 ++--- 6 files changed, 90 insertions(+), 36 deletions(-) diff --git a/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json index cfa2f27d1a..9e7f72a722 100644 --- a/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json +++ b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json @@ -1,11 +1,12 @@ { + "actions": [], "creation": "2019-09-23 14:36:36.935869", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ "status", - "scheduled_job", + "scheduled_job_type", "details" ], "fields": [ @@ -20,7 +21,13 @@ "reqd": 1 }, { - "fieldname": "scheduled_job", + "fieldname": "details", + "fieldtype": "Code", + "label": "Details", + "read_only": 1 + }, + { + "fieldname": "scheduled_job_type", "fieldtype": "Link", "in_list_view": 1, "in_standard_filter": 1, @@ -28,15 +35,10 @@ "options": "Scheduled Job Type", "read_only": 1, "reqd": 1 - }, - { - "fieldname": "details", - "fieldtype": "Code", - "label": "Details", - "read_only": 1 } ], - "modified": "2019-09-23 14:36:36.935869", + "links": [], + "modified": "2019-09-25 11:55:10.646458", "modified_by": "Administrator", "module": "Core", "name": "Scheduled Job Log", diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json index ec68544ab5..d35b88e9de 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json @@ -69,10 +69,10 @@ "links": [ { "link_doctype": "Scheduled Job Log", - "link_fieldname": "scheduled_job" + "link_fieldname": "scheduled_job_type" } ], - "modified": "2019-09-24 11:45:34.748779", + "modified": "2019-09-25 11:54:58.013623", "modified_by": "Administrator", "module": "Core", "name": "Scheduled Job Type", diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py index 7c6b2a0e0c..9325d62078 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -35,6 +35,10 @@ class ScheduledJobType(Document): queue = self.get_queue_name(), job_type=self.method) return True + else: + pass + #print('not yet due') + return False def is_event_due(self, current_time = None): @@ -90,7 +94,7 @@ class ScheduledJobType(Document): if not self.create_log: return if not self.scheduler_log: - self.scheduler_log = frappe.get_doc(dict(doctype = 'Scheduled Job Log', scheduled_job=self.name)).insert(ignore_permissions=True) + self.scheduler_log = frappe.get_doc(dict(doctype = 'Scheduled Job Log', scheduled_job_type=self.name)).insert(ignore_permissions=True) self.scheduler_log.db_set('status', status) if status == 'Failed': self.scheduler_log.db_set('details', frappe.get_traceback()) @@ -112,6 +116,7 @@ def execute_event(doc): def run_scheduled_job(job_type): '''This is a wrapper function that runs a hooks.scheduler_events method''' try: + print('executing job {}'.format(job_type)) frappe.get_doc('Scheduled Job Type', dict(method=job_type)).execute() except Exception: print(frappe.get_traceback()) diff --git a/frappe/database/database.py b/frappe/database/database.py index 1e6a85236e..5779c77e86 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -15,7 +15,7 @@ import frappe.model.meta from frappe import _ from time import time -from frappe.utils import now, getdate, cast_fieldtype +from frappe.utils import now, getdate, cast_fieldtype, get_datetime from frappe.utils.background_jobs import execute_job, get_queue from frappe.model.utils.link_count import flush_local_link_count from frappe.utils import cint @@ -940,6 +940,16 @@ class Database(object): else: frappe.throw(_('No conditions provided')) + def get_last_created(self, doctype): + last_record = self.get_all(doctype, ('creation'), limit=1, order_by='creation desc') + if last_record: + return get_datetime(last_record[0].creation) + else: + return None + + def clear_table(self, doctype): + self.sql('truncate `tab{}`'.format(doctype)) + def log_touched_tables(self, query, values=None): if values: query = frappe.safe_decode(self._cursor.mogrify(query, values)) diff --git a/frappe/tests/test_scheduler.py b/frappe/tests/test_scheduler.py index b8c46d376f..3617a3983b 100644 --- a/frappe/tests/test_scheduler.py +++ b/frappe/tests/test_scheduler.py @@ -4,15 +4,21 @@ from unittest import TestCase from dateutil.relativedelta import relativedelta from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs from frappe.utils.background_jobs import enqueue, get_jobs -from frappe.utils.scheduler import enqueue_events +from frappe.utils.scheduler import enqueue_events, is_dormant, schedule_jobs_based_on_activity +from frappe.utils import add_days, get_datetime import frappe import time def test_timeout(): - '''This function needs to be pickleable''' time.sleep(100) +def test_timeout_10(): + time.sleep(10) + +def test_method(): + pass + class TestScheduler(TestCase): def setUp(self): if not frappe.get_all('Scheduled Job Type', limit=1): @@ -30,24 +36,38 @@ class TestScheduler(TestCase): self.assertTrue('frappe.email.doctype.auto_email_report.auto_email_report.send_monthly', frappe.flags.enqueued_jobs) def test_queue_peeking(self): - if not frappe.db.exists('Scheduled Job Type', 'test_scheduler.test_timeout'): - job = frappe.get_doc(dict( - doctype = 'Scheduled Job Type', - method = 'frappe.tests.test_scheduler.test_timeout', - last_execution = '2010-01-01 00:00:00', - queue = 'All' - )).insert() - frappe.db.commit() - else: - job = frappe.get_doc('Scheduled Job Type', 'test_scheduler.test_timeout') - + job = get_test_job() self.assertTrue(job.enqueue()) job.db_set('last_execution', '2010-01-01 00:00:00') frappe.db.commit() - time.sleep(1) # wait if job is not yet queued + time.sleep(3) # wait if job is not yet queued self.assertFalse(job.enqueue()) job.delete() + def test_is_dormant(self): + self.assertTrue(is_dormant(check_time= get_datetime('2100-01-01 00:00:00'))) + self.assertTrue(is_dormant(check_time = add_days(frappe.db.get_last_created('Activity Log'), 5))) + self.assertFalse(is_dormant(check_time = frappe.db.get_last_created('Activity Log'))) + + def test_once_a_day_for_dormant(self): + frappe.db.clear_table('Scheduled Job Log') + self.assertTrue(schedule_jobs_based_on_activity(check_time= get_datetime('2100-01-01 00:00:00'))) + self.assertTrue(schedule_jobs_based_on_activity(check_time = add_days(frappe.db.get_last_created('Activity Log'), 5))) + + # create a fake job executed 5 days from now + job = get_test_job(method='frappe.tests.test_scheduler.test_method', queue='Daily') + job.execute() + job_log = frappe.get_doc('Scheduled Job Log', dict(scheduled_job_type=job.name)) + job_log.db_set('creation', add_days(frappe.db.get_last_created('Activity Log'), 5)) + + # inactive site with recent job, don't run + self.assertFalse(schedule_jobs_based_on_activity(check_time = add_days(frappe.db.get_last_created('Activity Log'), 5))) + + # one more day has passed + self.assertTrue(schedule_jobs_based_on_activity(check_time = add_days(frappe.db.get_last_created('Activity Log'), 6))) + + frappe.db.rollback() + def test_job_timeout(self): return job = enqueue(test_timeout, timeout=10) @@ -59,3 +79,20 @@ class TestScheduler(TestCase): break self.assertTrue(job.is_failed) + +def get_test_job(method='frappe.tests.test_scheduler.test_timeout_10', queue='All'): + if not frappe.db.exists('Scheduled Job Type', dict(method=method)): + job = frappe.get_doc(dict( + doctype = 'Scheduled Job Type', + method = method, + last_execution = '2010-01-01 00:00:00', + queue = queue + )).insert() + frappe.db.commit() + else: + job = frappe.get_doc('Scheduled Job Type', dict(method=method)) + job.db_set('last_execution', '2010-01-01 00:00:00') + job.db_set('queue', queue) + + return job + diff --git a/frappe/utils/scheduler.py b/frappe/utils/scheduler.py index 4d9f782542..fb606d6d3d 100755 --- a/frappe/utils/scheduler.py +++ b/frappe/utils/scheduler.py @@ -108,16 +108,16 @@ def enable_scheduler(): def disable_scheduler(): toggle_scheduler(False) -def schedule_jobs_based_on_activity(): +def schedule_jobs_based_on_activity(check_time=None): '''Returns True for active sites defined by Activity Log Returns True for inactive sites once in 24 hours''' - if is_dormant(): + if is_dormant(check_time=check_time): # ensure last job is one day old - last_job = frappe.db.get_all('Scheduled Job Log', ('creation'), limit=1, order_by='creation desc') - if not last_job: + last_job_timestamp = frappe.db.get_last_created('Scheduled Job Log') + if not last_job_timestamp: return True else: - if (now_datetime() - get_datetime(last_job[0].creation)).seconds > 86400: + if ((check_time or now_datetime()) - last_job_timestamp).total_seconds() >= 86400: # one day is passed since jobs are run, so lets do this return True else: @@ -127,12 +127,12 @@ def schedule_jobs_based_on_activity(): # site active, lets run the jobs return True -def is_dormant(): - last_activity_log = frappe.db.get_all('Activity Log', ('modified'), limit=1, order_by='modified desc') +def is_dormant(check_time=None): + last_activity_log_timestamp = frappe.db.get_last_created('Activity Log') since = (frappe.get_system_settings('dormant_days') or 4) * 86400 - if not last_activity_log: + if not last_activity_log_timestamp: return True - if (now_datetime() - get_datetime(last_activity_log[0].modified)).seconds > since: + if ((check_time or now_datetime()) - last_activity_log_timestamp).total_seconds() >= since: return True return False \ No newline at end of file From 463d1c5ec5312c355b280959b2d0865997df3dcb Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 26 Sep 2019 12:22:37 +0530 Subject: [PATCH 012/226] fix(major): upgrade redis-py and rq and try and fix scheduler tests --- .../doctype/scheduled_job_type/scheduled_job_type.py | 7 +++---- frappe/tests/test_scheduler.py | 11 +++++++++-- frappe/utils/background_jobs.py | 12 +++--------- frappe/utils/redis_wrapper.py | 5 +++++ requirements.txt | 4 ++-- 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py index 9325d62078..dcdf810995 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -34,10 +34,10 @@ class ScheduledJobType(Document): enqueue('frappe.core.doctype.scheduled_job_type.scheduled_job_type.run_scheduled_job', queue = self.get_queue_name(), job_type=self.method) return True - + else: + pass else: pass - #print('not yet due') return False @@ -111,12 +111,11 @@ class ScheduledJobType(Document): def execute_event(doc): frappe.only_for('System Manager') doc = json.loads(doc) - frappe.get_doc('Scheduled Job Type', doc.get('name')).execute() + frappe.get_doc('Scheduled Job Type', doc.get('name')).enqueue() def run_scheduled_job(job_type): '''This is a wrapper function that runs a hooks.scheduler_events method''' try: - print('executing job {}'.format(job_type)) frappe.get_doc('Scheduled Job Type', dict(method=job_type)).execute() except Exception: print(frappe.get_traceback()) diff --git a/frappe/tests/test_scheduler.py b/frappe/tests/test_scheduler.py index 3617a3983b..4f4c918576 100644 --- a/frappe/tests/test_scheduler.py +++ b/frappe/tests/test_scheduler.py @@ -37,10 +37,17 @@ class TestScheduler(TestCase): def test_queue_peeking(self): job = get_test_job() + self.assertTrue(job.enqueue()) job.db_set('last_execution', '2010-01-01 00:00:00') frappe.db.commit() - time.sleep(3) # wait if job is not yet queued + + # 1 job in queue + self.assertTrue(job.enqueue()) + job.db_set('last_execution', '2010-01-01 00:00:00') + frappe.db.commit() + + # 2nd job not loaded self.assertFalse(job.enqueue()) job.delete() @@ -88,11 +95,11 @@ def get_test_job(method='frappe.tests.test_scheduler.test_timeout_10', queue='Al last_execution = '2010-01-01 00:00:00', queue = queue )).insert() - frappe.db.commit() else: job = frappe.get_doc('Scheduled Job Type', dict(method=method)) job.db_set('last_execution', '2010-01-01 00:00:00') job.db_set('queue', queue) + frappe.db.commit() return job diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py index 7681e76646..c1ac7581dc 100755 --- a/frappe/utils/background_jobs.py +++ b/frappe/utils/background_jobs.py @@ -119,6 +119,8 @@ def execute_job(site, method, event, job_name, kwargs, user=None, is_async=True, except: frappe.db.rollback() frappe.log_error(method_name) + frappe.db.commit() + print(frappe.get_traceback()) raise else: @@ -140,8 +142,6 @@ def start_worker(queue=None, quiet = False): with Connection(redis_connection): queues = get_queue_list(queue) logging_level = "INFO" - if quiet: - logging_level = "WARNING" Worker(queues, name=get_worker_name(queue)).work(logging_level = logging_level) def get_worker_name(queue): @@ -203,13 +203,7 @@ def get_queue_list(queue_list=None): def get_queue(queue, is_async=True): '''Returns a Queue object tied to a redis connection''' validate_queue(queue) - - kwargs = { - 'connection': get_redis_conn(), - 'async': is_async - } - - return Queue(queue, **kwargs) + return Queue(queue, connection=get_redis_conn(), is_async=is_async) def validate_queue(queue, default_queue_list=None): if not default_queue_list: diff --git a/frappe/utils/redis_wrapper.py b/frappe/utils/redis_wrapper.py index cb92ced5c6..204efc9d1e 100644 --- a/frappe/utils/redis_wrapper.py +++ b/frappe/utils/redis_wrapper.py @@ -141,6 +141,9 @@ class RedisWrapper(redis.Redis): return super(RedisWrapper, self).llen(self.make_key(key)) def hset(self, name, key, value, shared=False): + if key is None: + return + _name = self.make_key(name, shared=shared) # set in local @@ -164,6 +167,8 @@ class RedisWrapper(redis.Redis): if not _name in frappe.local.cache: frappe.local.cache[_name] = {} + if not key: return None + if key in frappe.local.cache[_name]: return frappe.local.cache[_name][key] diff --git a/requirements.txt b/requirements.txt index 84788c863e..a7b09b797d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ werkzeug semantic_version rauth>=0.6.2 requests -redis==2.10.6 +redis>=3.0 selenium babel==2.6.0 ipython @@ -26,7 +26,7 @@ bleach==2.1.4 bleach-whitelist Pillow beautifulsoup4 -rq==0.12.0 +rq>=1.1.0 schedule cryptography pyopenssl From 63e14f6ee2a80a1f55d6464c4974b56cc0f5951b Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 26 Sep 2019 12:37:47 +0530 Subject: [PATCH 013/226] fix(tests): strict redis --- frappe/twofactor.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frappe/twofactor.py b/frappe/twofactor.py index a539532f25..e60113215b 100644 --- a/frappe/twofactor.py +++ b/frappe/twofactor.py @@ -248,9 +248,7 @@ def get_link_for_qrcode(user, totp_uri): key = frappe.generate_hash(length=20) key_user = "{}_user".format(key) key_uri = "{}_uri".format(key) - lifespan = int(frappe.db.get_value('System Settings', 'System Settings', 'lifespan_qrcode_image')) - if lifespan<=0: - lifespan = 240 + lifespan = int(frappe.db.get_value('System Settings', 'System Settings', 'lifespan_qrcode_image')) or 240 frappe.cache().set_value(key_uri, totp_uri, expires_in_sec=lifespan) frappe.cache().set_value(key_user, user, expires_in_sec=lifespan) return get_url('/qrcode?k={}'.format(key)) @@ -387,7 +385,7 @@ def should_remove_barcode_image(barcode): '''Check if it's time to delete barcode image from server. ''' if isinstance(barcode, string_types): barcode = frappe.get_doc('File', barcode) - lifespan = frappe.db.get_value('System Settings', 'System Settings', 'lifespan_qrcode_image') + lifespan = frappe.db.get_value('System Settings', 'System Settings', 'lifespan_qrcode_image') or 240 if time_diff_in_seconds(get_datetime(), barcode.creation) > int(lifespan): return True return False From 74e2e051623ad0ec4d599169933f9fa00d095098 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 26 Sep 2019 14:00:55 +0530 Subject: [PATCH 014/226] fix(test): two_factor --- frappe/tests/test_twofactor.py | 4 ++-- frappe/utils/redis_wrapper.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/tests/test_twofactor.py b/frappe/tests/test_twofactor.py index 27129d9832..57c98d2aee 100644 --- a/frappe/tests/test_twofactor.py +++ b/frappe/tests/test_twofactor.py @@ -157,8 +157,8 @@ def create_http_request(): '''Get http request object.''' set_request(method='POST', path='login') enable_2fa() - frappe.form_dict['usr'] = 'test@erpnext.com' - frappe.form_dict['pwd'] = 'test' + frappe.form_dict['usr'] = 'test@example.com' + frappe.form_dict['pwd'] = 'Eastern_43A1W' frappe.local.form_dict['cmd'] = 'login' http_requests = HTTPRequest() return http_requests diff --git a/frappe/utils/redis_wrapper.py b/frappe/utils/redis_wrapper.py index 204efc9d1e..4af59bceb2 100644 --- a/frappe/utils/redis_wrapper.py +++ b/frappe/utils/redis_wrapper.py @@ -43,7 +43,7 @@ class RedisWrapper(redis.Redis): try: if expires_in_sec: - self.setex(key, pickle.dumps(val), expires_in_sec) + self.setex(key, expires_in_sec, pickle.dumps(val)) else: self.set(key, pickle.dumps(val)) From 2415eac7708d50f98cbb7af9ffd0806a4027e05e Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 26 Sep 2019 15:59:32 +0530 Subject: [PATCH 015/226] fix(minor): postgres compatibility --- frappe/tests/test_scheduler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/tests/test_scheduler.py b/frappe/tests/test_scheduler.py index 4f4c918576..0ce7265fd7 100644 --- a/frappe/tests/test_scheduler.py +++ b/frappe/tests/test_scheduler.py @@ -25,7 +25,7 @@ class TestScheduler(TestCase): sync_jobs() def test_enqueue_jobs(self): - frappe.db.sql('update `tabScheduled Job Type` set last_execution = "2010-01-01 00:00:00"') + frappe.db.sql("update `tabScheduled Job Type` set last_execution = '2010-01-01 00:00:00'") frappe.flags.execute_job = True enqueue_events(site = frappe.local.site) From a050312b1a35bf04c4c0e274b5a50b76ff8e48e5 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 27 Sep 2019 12:13:45 +0530 Subject: [PATCH 016/226] fix(linting): added .codacy.yml to ignore .sql files --- .codacy.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .codacy.yml diff --git a/.codacy.yml b/.codacy.yml new file mode 100644 index 0000000000..944b90d2ac --- /dev/null +++ b/.codacy.yml @@ -0,0 +1,2 @@ +exclude_paths: + - '*.sql' \ No newline at end of file From a93fcd3de6a2dd8f1337f5b62c12f109afa461ed Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 27 Sep 2019 12:46:07 +0530 Subject: [PATCH 017/226] fix(minor): rename "queue" to "frequency" --- .codacy.yml | 2 +- .../scheduled_job_type.json | 24 +++++++++---------- .../scheduled_job_type/scheduled_job_type.py | 18 ++++++-------- .../test_scheduled_job_type.py | 6 ++--- 4 files changed, 23 insertions(+), 27 deletions(-) diff --git a/.codacy.yml b/.codacy.yml index 944b90d2ac..4754a63e7e 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -1,2 +1,2 @@ exclude_paths: - - '*.sql' \ No newline at end of file + - '**.sql' \ No newline at end of file diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json index d35b88e9de..1aafdb47b0 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json @@ -13,7 +13,7 @@ "field_order": [ "stopped", "method", - "queue", + "frequency", "cron_format", "last_execution", "create_log" @@ -27,16 +27,6 @@ "read_only": 1, "reqd": 1 }, - { - "fieldname": "queue", - "fieldtype": "Select", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Queue", - "options": "All\nHourly\nDaily\nDaily Long\nWeekly\nWeekly Long\nMonthly\nMonthly Long\nCron\nYearly\nAnnual", - "read_only": 1, - "reqd": 1 - }, { "default": "0", "fieldname": "stopped", @@ -63,6 +53,16 @@ "fieldtype": "Data", "label": "Cron Format", "read_only": 1 + }, + { + "fieldname": "frequency", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Frequency", + "options": "All\nHourly\nDaily\nDaily Long\nWeekly\nWeekly Long\nMonthly\nMonthly Long\nCron\nYearly\nAnnual", + "read_only": 1, + "reqd": 1 } ], "in_create": 1, @@ -72,7 +72,7 @@ "link_fieldname": "scheduled_job_type" } ], - "modified": "2019-09-25 11:54:58.013623", + "modified": "2019-09-27 12:19:23.259989", "modified_by": "Administrator", "module": "Core", "name": "Scheduled Job Type", diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py index dcdf810995..38123cea33 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -16,7 +16,7 @@ class ScheduledJobType(Document): self.name = '.'.join(self.method.split('.')[-2:]) def validate(self): - if self.queue != 'All': + if self.frequency != 'All': # force logging for all events other than continuous ones (ALL) self.create_log = 1 @@ -34,10 +34,6 @@ class ScheduledJobType(Document): enqueue('frappe.core.doctype.scheduled_job_type.scheduled_job_type.run_scheduled_job', queue = self.get_queue_name(), job_type=self.method) return True - else: - pass - else: - pass return False @@ -69,7 +65,7 @@ class ScheduledJobType(Document): } if not self.cron_format: - self.cron_format = CRON_MAP[self.queue] + self.cron_format = CRON_MAP[self.frequency] return croniter(self.cron_format, get_datetime(self.last_execution)).get_next(datetime) @@ -105,7 +101,7 @@ class ScheduledJobType(Document): frappe.db.commit() def get_queue_name(self): - return 'long' if ('Long' in self.queue) else 'default' + return 'long' if ('Long' in self.frequency) else 'default' @frappe.whitelist() def execute_event(doc): @@ -145,16 +141,16 @@ def insert_cron_event(events, all_events): def insert_event_list(events, event_type, all_events): for event in events: all_events.append(event) - queue = event_type.replace('_', ' ').title() - insert_single_event(queue, event) + frequency = event_type.replace('_', ' ').title() + insert_single_event(frequency, event) -def insert_single_event(queue, event, cron_format = None): +def insert_single_event(frequency, event, cron_format = None): if not frappe.db.exists('Scheduled Job Type', dict(method=event)): frappe.get_doc(dict( doctype = 'Scheduled Job Type', method = event, cron_format = cron_format, - queue = queue + frequency = frequency )).insert() def clear_events(all_events, scheduler_events): diff --git a/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py index 75af876367..6a4530f6ff 100644 --- a/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py @@ -20,16 +20,16 @@ class TestScheduledJobType(unittest.TestCase): def test_sync_jobs(self): all_job = frappe.get_doc('Scheduled Job Type', dict(method='frappe.email.queue.flush')) - self.assertEqual(all_job.queue, 'All') + self.assertEqual(all_job.frequency, 'All') daily_job = frappe.get_doc('Scheduled Job Type', dict(method='frappe.email.queue.clear_outbox')) - self.assertEqual(daily_job.queue, 'Daily') + self.assertEqual(daily_job.frequency, 'Daily') # check if cron jobs are synced cron_job = frappe.get_doc('Scheduled Job Type', dict(method='frappe.oauth.delete_oauth2_data')) - self.assertEqual(cron_job.queue, 'Cron') + self.assertEqual(cron_job.frequency, 'Cron') self.assertEqual(cron_job.cron_format, '0/15 * * * *') def test_daily_job(self): From 2c54347076d9e1f4c983149618df75c2a77690e0 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 27 Sep 2019 13:13:06 +0530 Subject: [PATCH 018/226] fix(minor): rename "queue" to "frequency" --- frappe/tests/test_scheduler.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/tests/test_scheduler.py b/frappe/tests/test_scheduler.py index 0ce7265fd7..e554fd23be 100644 --- a/frappe/tests/test_scheduler.py +++ b/frappe/tests/test_scheduler.py @@ -62,7 +62,7 @@ class TestScheduler(TestCase): self.assertTrue(schedule_jobs_based_on_activity(check_time = add_days(frappe.db.get_last_created('Activity Log'), 5))) # create a fake job executed 5 days from now - job = get_test_job(method='frappe.tests.test_scheduler.test_method', queue='Daily') + job = get_test_job(method='frappe.tests.test_scheduler.test_method', frequency='Daily') job.execute() job_log = frappe.get_doc('Scheduled Job Log', dict(scheduled_job_type=job.name)) job_log.db_set('creation', add_days(frappe.db.get_last_created('Activity Log'), 5)) @@ -87,18 +87,18 @@ class TestScheduler(TestCase): self.assertTrue(job.is_failed) -def get_test_job(method='frappe.tests.test_scheduler.test_timeout_10', queue='All'): +def get_test_job(method='frappe.tests.test_scheduler.test_timeout_10', frequency='All'): if not frappe.db.exists('Scheduled Job Type', dict(method=method)): job = frappe.get_doc(dict( doctype = 'Scheduled Job Type', method = method, last_execution = '2010-01-01 00:00:00', - queue = queue + frequency = frequency )).insert() else: job = frappe.get_doc('Scheduled Job Type', dict(method=method)) job.db_set('last_execution', '2010-01-01 00:00:00') - job.db_set('queue', queue) + job.db_set('frequency', frequency) frappe.db.commit() return job From 8d4610504a243cd5bc55a9bd753ff919b5e0d031 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 6 Nov 2019 10:01:31 +0530 Subject: [PATCH 019/226] fix(notifications): login, logout notifications --- frappe/desk/notifications.py | 24 ++++++++++++++++-------- frappe/tests/test_api.py | 21 +++++++++++---------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index 84d515050c..0216ab83af 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -11,15 +11,19 @@ import json @frappe.whitelist() @frappe.read_only() def get_notifications(): + out = { + "open_count_doctype": {}, + "targets": {}, + } if (frappe.flags.in_install or not frappe.db.get_single_value('System Settings', 'setup_complete')): - return { - "open_count_doctype": {}, - "targets": {}, - } + return out config = get_notification_config() + if not config: + return out + groups = list(config.get("for_doctype")) + list(config.get("for_module")) cache = frappe.cache() @@ -31,10 +35,10 @@ def get_notifications(): if count is not None: notification_count[name] = count - return { - "open_count_doctype": get_notifications_for_doctypes(config, notification_count), - "targets": get_notifications_for_targets(config, notification_percent), - } + out['open_count_doctype'] = get_notifications_for_doctypes(config, notification_count) + out['targets'] = get_notifications_for_targets(config, notification_percent) + + return out def get_notifications_for_doctypes(config, notification_count): """Notifications for DocTypes""" @@ -118,6 +122,10 @@ def clear_notifications(user=None): return cache = frappe.cache() config = get_notification_config() + + if not config: + return + for_doctype = list(config.get('for_doctype')) if config.get('for_doctype') else [] for_module = list(config.get('for_module')) if config.get('for_module') else [] groups = for_doctype + for_module diff --git a/frappe/tests/test_api.py b/frappe/tests/test_api.py index 232b2be4a8..19ea9a0124 100644 --- a/frappe/tests/test_api.py +++ b/frappe/tests/test_api.py @@ -5,13 +5,14 @@ from __future__ import unicode_literals import unittest, frappe, os from frappe.core.doctype.user.user import generate_keys from frappe.frappeclient import FrappeClient +from frappe.utils.data import get_url import requests import base64 class TestAPI(unittest.TestCase): def test_insert_many(self): - server = FrappeClient(frappe.get_site_config().host_name, "Administrator", "admin", verify=False) + server = FrappeClient(get_url(), "Administrator", "admin", verify=False) frappe.db.sql("delete from `tabNote` where title in ('Sing','a','song','of','sixpence')") frappe.db.commit() @@ -30,7 +31,7 @@ class TestAPI(unittest.TestCase): self.assertTrue(frappe.db.get_value('Note', {'title': 'sixpence'})) def test_create_doc(self): - server = FrappeClient(frappe.get_site_config().host_name, "Administrator", "admin", verify=False) + server = FrappeClient(get_url(), "Administrator", "admin", verify=False) frappe.db.sql("delete from `tabNote` where title = 'test_create'") frappe.db.commit() @@ -39,13 +40,13 @@ class TestAPI(unittest.TestCase): self.assertTrue(frappe.db.get_value('Note', {'title': 'test_create'})) def test_list_docs(self): - server = FrappeClient(frappe.get_site_config().host_name, "Administrator", "admin", verify=False) + server = FrappeClient(get_url(), "Administrator", "admin", verify=False) doc_list = server.get_list("Note") self.assertTrue(len(doc_list)) def test_get_doc(self): - server = FrappeClient(frappe.get_site_config().host_name, "Administrator", "admin", verify=False) + server = FrappeClient(get_url(), "Administrator", "admin", verify=False) frappe.db.sql("delete from `tabNote` where title = 'get_this'") frappe.db.commit() @@ -56,7 +57,7 @@ class TestAPI(unittest.TestCase): self.assertTrue(doc) def test_update_doc(self): - server = FrappeClient(frappe.get_site_config().host_name, "Administrator", "admin", verify=False) + server = FrappeClient(get_url(), "Administrator", "admin", verify=False) frappe.db.sql("delete from `tabNote` where title in ('Sing','sing')") frappe.db.commit() @@ -68,7 +69,7 @@ class TestAPI(unittest.TestCase): self.assertTrue(doc["title"] == changed_title) def test_delete_doc(self): - server = FrappeClient(frappe.get_site_config().host_name, "Administrator", "admin", verify=False) + server = FrappeClient(get_url(), "Administrator", "admin", verify=False) frappe.db.sql("delete from `tabNote` where title = 'delete'") frappe.db.commit() @@ -90,21 +91,21 @@ class TestAPI(unittest.TestCase): api_key = frappe.db.get_value("User", "Administrator", "api_key") header = {"Authorization": "token {}:{}".format(api_key, generated_secret)} - res = requests.post(frappe.get_site_config().host_name + "/api/method/frappe.auth.get_logged_user", headers=header) + res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header) self.assertEqual(res.status_code, 200) self.assertEqual("Administrator", res.json()["message"]) self.assertEqual(keys['api_secret'], generated_secret) header = {"Authorization": "Basic {}".format(base64.b64encode(frappe.safe_encode("{}:{}".format(api_key, generated_secret))).decode())} - res = requests.post(frappe.get_site_config().host_name + "/api/method/frappe.auth.get_logged_user", headers=header) + res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header) self.assertEqual(res.status_code, 200) self.assertEqual("Administrator", res.json()["message"]) # Valid api key, invalid api secret api_secret = "ksk&93nxoe3os" header = {"Authorization": "token {}:{}".format(api_key, api_secret)} - res = requests.post(frappe.get_site_config().host_name + "/api/method/frappe.auth.get_logged_user", headers=header) + res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header) self.assertEqual(res.status_code, 403) @@ -112,5 +113,5 @@ class TestAPI(unittest.TestCase): api_key = "@3djdk3kld" api_secret = "ksk&93nxoe3os" header = {"Authorization": "token {}:{}".format(api_key, api_secret)} - res = requests.post(frappe.get_site_config().host_name + "/api/method/frappe.auth.get_logged_user", headers=header) + res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header) self.assertEqual(res.status_code, 401) From d1e461623199a4918752bf524f3bfb83d4fdcf74 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 7 Nov 2019 12:34:38 +0530 Subject: [PATCH 020/226] fix(test): the test event has hanged frequency --- .../core/doctype/scheduled_job_type/test_scheduled_job_type.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py index 6a4530f6ff..ec1e70ad6a 100644 --- a/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py @@ -40,7 +40,7 @@ class TestScheduledJobType(unittest.TestCase): self.assertFalse(job.is_event_due(get_datetime('2019-01-01 23:59:59'))) def test_weekly_job(self): - job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.utils.change_log.check_for_update')) + job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.social.doctype.energy_point_log.energy_point_log.send_weekly_summary')) job.db_set('last_execution', '2019-01-01 00:00:00') self.assertTrue(job.is_event_due(get_datetime('2019-01-06 00:00:01'))) self.assertFalse(job.is_event_due(get_datetime('2019-01-02 00:00:06'))) From 0c77540ed4fad1f138d73ac3518e6367092ec0d6 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 7 Nov 2019 15:07:56 +0530 Subject: [PATCH 021/226] fix(ux): added dropdown in print layout to avoid linebreak and fix for login --- frappe/desk/notifications.py | 2 ++ .../frappe/form/templates/print_layout.html | 25 +++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index 0216ab83af..b142047059 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -147,6 +147,8 @@ def delete_notification_count_for(doctype): def clear_doctype_notifications(doc, method=None, *args, **kwargs): config = get_notification_config() + if not config: + return if isinstance(doc, string_types): doctype = doc # assuming doctype name was passed directly else: diff --git a/frappe/public/js/frappe/form/templates/print_layout.html b/frappe/public/js/frappe/form/templates/print_layout.html index 9bdba0d99d..e4197aa0a2 100644 --- a/frappe/public/js/frappe/form/templates/print_layout.html +++ b/frappe/public/js/frappe/form/templates/print_layout.html @@ -15,18 +15,27 @@ From da2d1d7bb1483a947b865a7613783d9c8a69d32f Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Sun, 10 Nov 2019 10:22:16 +0530 Subject: [PATCH 022/226] fix: add index on child table --- .../doctype/global_search_doctype/global_search_doctype.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frappe/desk/doctype/global_search_doctype/global_search_doctype.py b/frappe/desk/doctype/global_search_doctype/global_search_doctype.py index 4c9a948278..bf3b4d3040 100644 --- a/frappe/desk/doctype/global_search_doctype/global_search_doctype.py +++ b/frappe/desk/doctype/global_search_doctype/global_search_doctype.py @@ -3,8 +3,10 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe from frappe.model.document import Document class GlobalSearchDocType(Document): - pass + + def on_doctype_update(): + frappe.db.add_index("Global Search DocType", ["document_type"]) From 9c3132753c35f50de6b88a677453790b684da7c6 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 8 Nov 2019 22:20:23 +0530 Subject: [PATCH 023/226] feat: Child Table pagination --- frappe/public/js/frappe/form/form.js | 6 +- frappe/public/js/frappe/form/grid.js | 252 +++++++++++------- .../public/js/frappe/form/grid_pagination.js | 127 +++++++++ frappe/public/less/form_grid.less | 26 ++ 4 files changed, 313 insertions(+), 98 deletions(-) create mode 100644 frappe/public/js/frappe/form/grid_pagination.js diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 5a983986d8..d565435a83 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -328,7 +328,11 @@ frappe.ui.form.Form = class FrappeForm { } } // reset visible columns, since column headings can change in different docs - this.grids.forEach(grid_obj => grid_obj.grid.visible_columns = null); + this.grids.forEach(grid_obj => { + grid_obj.grid.visible_columns = null + // reset page number to 1 + grid_obj.grid.grid_pagination.go_to_page(1); + }); frappe.ui.form.close_grid_form(); this.docname = docname; } diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 90e0f90be7..dcbfac33a8 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -2,6 +2,7 @@ // MIT License. See license.txt import GridRow from "./grid_row"; +import GridPagination from './grid_pagination' frappe.ui.form.get_open_grid_form = function() { return $(".grid-row-open").data("grid_row"); @@ -63,25 +64,37 @@ export default class Grid {