diff --git a/.codacy.yml b/.codacy.yml new file mode 100644 index 0000000000..4754a63e7e --- /dev/null +++ b/.codacy.yml @@ -0,0 +1,2 @@ +exclude_paths: + - '**.sql' \ No newline at end of file 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/.travis.yml b/.travis.yml index df66db88a7..6990a0df8a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python dist: trusty -sudo: required addons: hosts: @@ -39,6 +38,16 @@ matrix: env: DB=mariadb TYPE=server script: bench --site test_site run-tests --coverage +before_install: + # install wkhtmltopdf + - wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz + - tar -xf /tmp/wkhtmltox.tar.xz -C /tmp + - sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf + - sudo chmod o+x /usr/local/bin/wkhtmltopdf + + # install cups + - sudo apt-get install libcups2-dev + install: - cd ~ - source ./.nvm/nvm.sh @@ -52,23 +61,20 @@ install: - mkdir ~/frappe-bench/sites/test_site - cp $TRAVIS_BUILD_DIR/.travis/$DB.json ~/frappe-bench/sites/test_site/site_config.json - - mysql -u root -e "SET GLOBAL character_set_server = 'utf8mb4'" - - mysql -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" + - if [ $DB == "mariadb" ];then + mysql -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"; + mysql -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"; + mysql -u root -e "CREATE DATABASE test_frappe"; + mysql -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"; + mysql -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"; + mysql -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"; + mysql -u root -e "FLUSH PRIVILEGES"; + fi - - mysql -u root -e "CREATE DATABASE test_frappe" - - mysql -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'" - - mysql -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'" - - - mysql -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'" - - mysql -u root -e "FLUSH PRIVILEGES" - - - psql -c "CREATE DATABASE test_frappe" -U postgres - - psql -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres - - - wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz - - tar -xf /tmp/wkhtmltox.tar.xz -C /tmp - - sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf - - sudo chmod o+x /usr/local/bin/wkhtmltopdf + - if [ $DB == "postgres" ];then + psql -c "CREATE DATABASE test_frappe" -U postgres; + psql -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres; + fi - cd ./frappe-bench diff --git a/cypress/integration/list_view.js b/cypress/integration/list_view.js index 20a973c1dd..d6627ea9c4 100644 --- a/cypress/integration/list_view.js +++ b/cypress/integration/list_view.js @@ -2,10 +2,9 @@ context('List View', () => { before(() => { cy.login(); cy.visit('/desk'); - cy.window().its('frappe').then(frappe => { - frappe.call("frappe.tests.ui_test_helpers.setup_workflow"); + return cy.window().its('frappe').then(frappe => { + return frappe.xcall("frappe.tests.ui_test_helpers.setup_workflow"); }); - cy.clear_cache(); }); it('enables "Actions" button', () => { const actions = ['Approve', 'Reject', 'Edit', 'Assign To', 'Apply Assignment Rule', 'Print', 'Delete']; diff --git a/frappe/__init__.py b/frappe/__init__.py index 18ec5c0ee7..cb1b6e5358 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, @@ -1508,7 +1507,22 @@ 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 + else: + error = get_traceback() + + 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/api.py b/frappe/api.py index b61c535edd..95a9a408a5 100644 --- a/frappe/api.py +++ b/frappe/api.py @@ -82,7 +82,7 @@ def handle(): if frappe.local.request.method=="PUT": if frappe.local.form_dict.data is None: - data = json.loads(frappe.local.request.get_data()) + data = json.loads(frappe.safe_decode(frappe.local.request.get_data())) else: data = json.loads(frappe.local.form_dict.data) doc = frappe.get_doc(doctype, name) @@ -117,7 +117,7 @@ def handle(): if frappe.local.request.method=="POST": if frappe.local.form_dict.data is None: - data = json.loads(frappe.local.request.get_data()) + data = json.loads(frappe.safe_decode(frappe.local.request.get_data())) else: data = json.loads(frappe.local.form_dict.data) data.update({ diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index a950669d12..27f17a1a62 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -105,10 +105,6 @@ class AutoRepeat(Document): schedule_details = [] start_date = getdate(self.start_date) end_date = getdate(self.end_date) - today = frappe.utils.datetime.date.today() - - if start_date < today: - start_date = today if not self.end_date: start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day) @@ -121,7 +117,8 @@ class AutoRepeat(Document): start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day) if self.end_date: - start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day) + start_date = start_date = get_next_schedule_date( + start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, end_date, for_full_schedule=True) while (getdate(start_date) < getdate(end_date)): row = { "reference_document" : self.reference_document, @@ -129,7 +126,8 @@ class AutoRepeat(Document): "next_scheduled_date" : start_date } schedule_details.append(row) - start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, end_date) + start_date = start_date = get_next_schedule_date( + start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, end_date, for_full_schedule=True) return schedule_details @@ -271,18 +269,28 @@ class AutoRepeat(Document): ) -def get_next_schedule_date(start_date, frequency, repeat_on_day, repeat_on_last_day = False, end_date = None): +def get_next_schedule_date(start_date, frequency, repeat_on_day, repeat_on_last_day=False, end_date=None, for_full_schedule=False): month_count = month_map.get(frequency) + day_count = 0 if month_count and repeat_on_last_day: next_date = get_next_date(start_date, month_count, 31) + day_count = 31 + next_date = get_next_date(start_date, month_count, day_count) elif month_count and repeat_on_day: next_date = get_next_date(start_date, month_count, repeat_on_day) + day_count = repeat_on_day + next_date = get_next_date(start_date, month_count, day_count) elif month_count: next_date = get_next_date(start_date, month_count) else: days = 7 if frequency == 'Weekly' else 1 next_date = add_days(start_date, days) + # next schedule date should be after or on current date + if not for_full_schedule: + while getdate(next_date) < getdate(today()): + next_date = get_next_date(next_date, month_count, day_count) + return next_date def get_next_date(dt, mcount, day=None): @@ -307,7 +315,7 @@ def create_repeated_entries(data): current_date = getdate(today()) schedule_date = getdate(doc.next_schedule_date) - while schedule_date <= current_date and not doc.disabled: + if schedule_date == current_date and not doc.disabled: doc.create_documents() schedule_date = get_next_schedule_date(schedule_date, doc.frequency, doc.repeat_on_day, doc.repeat_on_last_day, doc.end_date) diff --git a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py index cb98d4a8fd..95f95f3e7d 100644 --- a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py @@ -96,6 +96,21 @@ class TestAutoRepeat(unittest.TestCase): linked_comm = frappe.db.exists("Communication", dict(reference_doctype="ToDo", reference_name=new_todo)) self.assertTrue(linked_comm) + def test_next_schedule_date(self): + current_date = getdate(today()) + todo = frappe.get_doc( + dict(doctype='ToDo', description='test next schedule date todo', assigned_by='Administrator')).insert() + doc = make_auto_repeat(frequency='Monthly', reference_document=todo.name, start_date=add_months(today(), -2)) + + #check next_schedule_date is set as per current date + #it should not be a previous month's date + self.assertEqual(doc.next_schedule_date, current_date) + data = get_auto_repeat_entries(current_date) + create_repeated_entries(data) + docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name}) + #the original doc + the repeated doc + self.assertEqual(len(docnames), 2) + def make_auto_repeat(**args): args = frappe._dict(args) diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 9a408430e7..d29f0a9c97 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -507,26 +507,6 @@ def run_ui_tests(context, app, headless=False): formatted_command = command.format(site_env=site_env, password_env=password_env, run_or_open=run_or_open) frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True) -@click.command('run-setup-wizard-ui-test') -@click.option('--app', help="App to run tests on, leave blank for all apps") -@click.option('--profile', is_flag=True, default=False) -@pass_context -def run_setup_wizard_ui_test(context, app=None, profile=False): - "Run setup wizard UI test" - import frappe.test_runner - - site = get_site(context) - frappe.init(site=site) - frappe.connect() - - ret = frappe.test_runner.run_setup_wizard_ui_test(app=app, verbose=context.verbose, - profile=profile) - if len(ret.failures) == 0 and len(ret.errors) == 0: - ret = 0 - - if os.environ.get('CI'): - sys.exit(ret) - @click.command('serve') @click.option('--port', default=8000) @click.option('--profile', is_flag=True, default=False) @@ -752,7 +732,6 @@ commands = [ reset_perms, run_tests, run_ui_tests, - run_setup_wizard_ui_test, serve, set_config, show_config, 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..4e3f2fd84a 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", @@ -28,6 +29,7 @@ "name_case", "column_break_15", "description", + "documentation", "form_settings_section", "image_field", "timeline_field", @@ -57,6 +59,10 @@ "restrict_to_domain", "read_only", "in_create", + "actions_section", + "actions", + "links_section", + "links", "web_view", "has_web_view", "allow_guest_to_view", @@ -454,11 +460,39 @@ "fieldname": "nsm_parent_field", "fieldtype": "Data", "label": "Parent Field (Tree)" + }, + { + "description": "URL for documentation or help", + "fieldname": "documentation", + "fieldtype": "Data", + "label": "Documentation Link" + }, + { + "fieldname": "actions_section", + "fieldtype": "Section Break", + "label": "Actions" + }, + { + "fieldname": "actions", + "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-07 14:28:05.392490", + "modified": "2019-11-25 17:24:03.690192", "modified_by": "Administrator", "module": "Core", "name": "DocType", diff --git a/frappe/core/doctype/test_runner/__init__.py b/frappe/core/doctype/doctype_action/__init__.py similarity index 100% rename from frappe/core/doctype/test_runner/__init__.py rename to frappe/core/doctype/doctype_action/__init__.py 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..7a1b845af3 --- /dev/null +++ b/frappe/core/doctype/doctype_action/doctype_action.json @@ -0,0 +1,57 @@ +{ + "actions": [], + "creation": "2019-09-23 16:28:13.953520", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label", + "action_type", + "action", + "group" + ], + "fields": [ + { + "columns": 2, + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "reqd": 1 + }, + { + "fieldname": "group", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Group" + }, + { + "columns": 2, + "fieldname": "action_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Action Type", + "options": "Server Action", + "reqd": 1 + }, + { + "columns": 4, + "fieldname": "action", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Action", + "reqd": 1 + } + ], + "istable": 1, + "modified": "2019-09-24 09:11:39.860100", + "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/tests/ui/__init__.py b/frappe/core/doctype/doctype_link/__init__.py similarity index 100% rename from frappe/tests/ui/__init__.py rename to frappe/core/doctype/doctype_link/__init__.py 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_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..9e7f72a722 --- /dev/null +++ b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json @@ -0,0 +1,64 @@ +{ + "actions": [], + "creation": "2019-09-23 14:36:36.935869", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "status", + "scheduled_job_type", + "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": "details", + "fieldtype": "Code", + "label": "Details", + "read_only": 1 + }, + { + "fieldname": "scheduled_job_type", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Scheduled Job", + "options": "Scheduled Job Type", + "read_only": 1, + "reqd": 1 + } + ], + "links": [], + "modified": "2019-09-25 11:55:10.646458", + "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..1aafdb47b0 --- /dev/null +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json @@ -0,0 +1,98 @@ +{ + "actions": [ + { + "action": "frappe.core.doctype.scheduled_job_type.scheduled_job_type.execute_event", + "action_type": "Server Action", + "label": "Execute" + } + ], + "creation": "2019-09-23 14:34:09.205368", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "stopped", + "method", + "frequency", + "cron_format", + "last_execution", + "create_log" + ], + "fields": [ + { + "fieldname": "method", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Method", + "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 + }, + { + "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, + "links": [ + { + "link_doctype": "Scheduled Job Log", + "link_fieldname": "scheduled_job_type" + } + ], + "modified": "2019-09-27 12:19:23.259989", + "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..38123cea33 --- /dev/null +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -0,0 +1,159 @@ +# -*- 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, get_jobs + +class ScheduledJobType(Document): + def autoname(self): + self.name = '.'.join(self.method.split('.')[-2:]) + + def validate(self): + if self.frequency != '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() + 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 + + return False + + 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 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 *", + "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.frequency] + + return croniter(self.cron_format, + get_datetime(self.last_execution)).get_next(datetime) + + def execute(self): + self.scheduler_log = None + try: + self.log_status('Start') + frappe.get_attr(self.method)() + frappe.db.commit() + self.log_status('Complete') + except Exception: + frappe.db.rollback() + 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_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()) + frappe.db.commit() + + 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 'long' if ('Long' in self.frequency) else 'default' + +@frappe.whitelist() +def execute_event(doc): + frappe.only_for('System Manager') + doc = json.loads(doc) + 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: + 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') + 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) + frequency = event_type.replace('_', ' ').title() + insert_single_event(frequency, event) + +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, + frequency = frequency + )).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.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..ec1e70ad6a --- /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.frequency, 'All') + + daily_job = frappe.get_doc('Scheduled Job Type', + dict(method='frappe.email.queue.clear_outbox')) + 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.frequency, '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.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'))) + 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/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/core/doctype/test_runner/_test_test_runner.js b/frappe/core/doctype/test_runner/_test_test_runner.js deleted file mode 100644 index 0b0bd9a98b..0000000000 --- a/frappe/core/doctype/test_runner/_test_test_runner.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Test Runner", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially('Test Runner', [ - // insert a new Test Runner - () => frappe.tests.make([ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/frappe/core/doctype/test_runner/test_runner.js b/frappe/core/doctype/test_runner/test_runner.js deleted file mode 100644 index d08a3626a3..0000000000 --- a/frappe/core/doctype/test_runner/test_runner.js +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Test Runner', { - refresh: (frm) => { - frm.disable_save(); - frm.page.set_primary_action(__("Run Tests"), () => { - return new Promise(resolve => { - let wrapper = $(frm.fields_dict.output.wrapper).empty(); - $("
Loading...
").appendTo(wrapper); - - // all tests - frappe.call({ - method: 'frappe.core.doctype.test_runner.test_runner.get_test_js', - args: { test_path: frm.doc.module_path } - }).always((data) => { - $("").appendTo(wrapper.empty()); - frm.events.run_tests(frm, data.message); - resolve(); - }); - }); - }); - - }, - run_tests: function(frm, files) { - frappe.flags.in_test = true; - let require_list = [ - "assets/frappe/js/lib/jquery/qunit.js", - "assets/frappe/js/lib/jquery/qunit.css" - ].concat(); - - frappe.require(require_list, () => { - files.forEach((f) => { - frappe.dom.eval(f.script); - }); - - QUnit.config.notrycatch = true; - - window.onerror = function(msg, url, lineNo, columnNo, error) { - console.log(error.stack); // eslint-disable-line - $('').appendTo($('body')); - }; - - QUnit.testDone(function(details) { - // var result = { - // "Module name": details.module, - // "Test name": details.name, - // "Assertions": { - // "Total": details.total, - // "Passed": details.passed, - // "Failed": details.failed - // }, - // "Skipped": details.skipped, - // "Todo": details.todo, - // "Runtime": details.runtime - // }; - - // eslint-disable-next-line - // console.log(JSON.stringify(result, null, 2)); - - details.assertions.map(a => { - // eslint-disable-next-line - console.log(`${a.result ? '✔' : '✗'} ${a.message}`); - }); - - }); - QUnit.load(); - - QUnit.done(({ total, failed, passed, runtime }) => { - // flag for selenium that test is done - - console.log( `Total: ${total}, Failed: ${failed}, Passed: ${passed}, Runtime: ${runtime}` ); // eslint-disable-line - - if(failed) { - console.log('Tests Failed'); // eslint-disable-line - } else { - console.log('Tests Passed'); // eslint-disable-line - } - frappe.set_route('Form', 'Test Runner', 'Test Runner'); - - $('').appendTo($('body')); - - }); - }); - - } -}); diff --git a/frappe/core/doctype/test_runner/test_runner.json b/frappe/core/doctype/test_runner/test_runner.json deleted file mode 100644 index ccc1361dc9..0000000000 --- a/frappe/core/doctype/test_runner/test_runner.json +++ /dev/null @@ -1,152 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-06-26 10:57:19.976624", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "module_path", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Module Path", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "app", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "App", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "output", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Output", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2017-07-19 03:22:33.221169", - "modified_by": "Administrator", - "module": "Core", - "name": "Test Runner", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "Administrator", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/frappe/core/doctype/test_runner/test_runner.py b/frappe/core/doctype/test_runner/test_runner.py deleted file mode 100644 index 2961e9f38b..0000000000 --- a/frappe/core/doctype/test_runner/test_runner.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2017, Frappe Technologies and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe, os -from frappe.model.document import Document - -class TestRunner(Document): - pass - -@frappe.whitelist() -def get_test_js(test_path=None): - '''Get test + data for app, example: app/tests/ui/test_name.js''' - if not test_path: - test_path = frappe.db.get_single_value('Test Runner', 'module_path') - test_js = [] - - # split - app, test_path = test_path.split(os.path.sep, 1) - - # now full path - test_path = frappe.get_app_path(app, test_path) - - def add_file(path): - with open(path, 'r') as fileobj: - test_js.append(dict( - script = fileobj.read() - )) - - # add test_lib.js - add_file(frappe.get_app_path('frappe', 'tests', 'ui', 'data', 'test_lib.js')) - add_file(test_path) - - return test_js - diff --git a/frappe/database/database.py b/frappe/database/database.py index f5055571ff..1c08dd714e 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 @@ -941,6 +941,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/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql index 7058ed0325..b1a769b189 100644 --- a/frappe/database/mariadb/framework_mariadb.sql +++ b/frappe/database/mariadb/framework_mariadb.sql @@ -105,6 +105,53 @@ CREATE TABLE `tabDocPerm` ( KEY `parent` (`parent`) ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +-- +-- Table structure for 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, + `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 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 df59de92df..cd2f02d8e4 100644 --- a/frappe/database/postgres/framework_postgres.sql +++ b/frappe/database/postgres/framework_postgres.sql @@ -106,6 +106,57 @@ 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) NOT NULL, + "group" varchar(140) DEFAULT NULL, + "action_type" varchar(140) NOT NULL, + "action" varchar(140) NOT NULL, + PRIMARY KEY ("name") +) ; + +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 NULL, + "group" varchar(140) DEFAULT NULL, + "link_doctype" varchar(140) NOT NULL, + "link_fieldname" varchar(140) NOT NULL, + PRIMARY KEY ("name") +) ; + +create index on "tabDocType Link" ("parent"); + + -- -- Table structure for table "tabDocType" -- diff --git a/frappe/desk/doctype/setup_wizard_help_link/__init__.py b/frappe/desk/doctype/setup_wizard_help_link/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/setup_wizard_help_link/setup_wizard_help_link.json b/frappe/desk/doctype/setup_wizard_help_link/setup_wizard_help_link.json new file mode 100644 index 0000000000..b97b482cac --- /dev/null +++ b/frappe/desk/doctype/setup_wizard_help_link/setup_wizard_help_link.json @@ -0,0 +1,35 @@ +{ + "creation": "2019-11-19 12:22:42.805741", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label", + "video_id" + ], + "fields": [ + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label" + }, + { + "fieldname": "video_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Video" + } + ], + "istable": 1, + "modified": "2019-11-19 13:39:57.716248", + "modified_by": "Administrator", + "module": "Desk", + "name": "Setup Wizard Help Link", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/desk/doctype/setup_wizard_help_link/setup_wizard_help_link.py b/frappe/desk/doctype/setup_wizard_help_link/setup_wizard_help_link.py new file mode 100644 index 0000000000..fbb96bbd91 --- /dev/null +++ b/frappe/desk/doctype/setup_wizard_help_link/setup_wizard_help_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 SetupWizardHelpLink(Document): + pass diff --git a/frappe/desk/doctype/setup_wizard_slide/__init__.py b/frappe/desk/doctype/setup_wizard_slide/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.js b/frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.js new file mode 100644 index 0000000000..7a802ad2b6 --- /dev/null +++ b/frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.js @@ -0,0 +1,45 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Setup Wizard Slide', { + refresh: function(frm) { + frm.toggle_reqd('ref_doctype', frm.doc.slide_type!='Information'); + frm.toggle_reqd('slide_module', frm.doc.slide_type=='Information'); + }, + + ref_doctype: function(frm) { + frm.set_query('ref_doctype', function() { + if (frm.doc.slide_type === 'Create') { + return { + filters: { + 'issingle': 0, + 'istable': 0 + } + }; + } else if (frm.doc.slide_type === 'Settings') { + return { + filters: { + 'issingle': 1, + 'istable': 0 + } + }; + } + }); + + //fetch mandatory fields automatically + if (frm.doc.ref_doctype) { + frappe.model.clear_table(frm.doc, 'slide_fields'); + let fields = frappe.meta.get_docfields(frm.doc.ref_doctype, null, { + reqd: 1 + }); + $.each(fields, function(_i, data) { + let row = frappe.model.add_child(frm.doc, 'Setup Wizard Slide', 'slide_fields'); + row.label = data.label; + row.fieldtype = data.fieldtype; + row.fieldname = data.fieldname; + row.options = data.options; + }); + refresh_field('slide_fields'); + } + } +}); diff --git a/frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.json b/frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.json new file mode 100644 index 0000000000..94d94851c3 --- /dev/null +++ b/frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.json @@ -0,0 +1,182 @@ +{ + "autoname": "field:slide_title", + "creation": "2019-11-13 14:39:56.834658", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "slide_title", + "app", + "slide_order", + "column_break_4", + "image_src", + "slide_module", + "description_section_break", + "slide_desc", + "action_section_break", + "slide_type", + "submit_method", + "column_break_6", + "max_count", + "add_more_button", + "section_break_18", + "ref_doctype", + "slide_fields", + "section_break_10", + "domains", + "column_break_12", + "help_links" + ], + "fields": [ + { + "fieldname": "slide_title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Slide Title", + "reqd": 1, + "unique": 1 + }, + { + "depends_on": "eval:doc.slide_type!='Information'", + "description": "By default the code inside `create_onboarding_docs` method of the `Reference Document Type` is executed. If your method is not on the doctype level, place this method in {app_name}.utilities.onboarding_utils.{method_name} and specify the method name here", + "fieldname": "submit_method", + "fieldtype": "Data", + "label": "Submit Method" + }, + { + "fieldname": "slide_desc", + "fieldtype": "HTML Editor", + "label": "Slide Description" + }, + { + "default": "3", + "depends_on": "add_more_button", + "description": "The amount of times you want to repeat the set of fields (eg: if you want 3 customers in the slide, set this field to 3. Only the first set of fields is shown as mandatory in the slide)", + "fieldname": "max_count", + "fieldtype": "Int", + "label": "Max Count" + }, + { + "default": "0", + "depends_on": "eval:doc.slide_type!='Information'", + "fieldname": "add_more_button", + "fieldtype": "Check", + "label": "Add More Button" + }, + { + "depends_on": "eval:doc.slide_type!='Information'", + "fieldname": "slide_fields", + "fieldtype": "Table", + "label": "Slide Fields", + "options": "Setup Wizard Slide Field" + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, + { + "description": "Specify in what all domains should the slides show up. If nothing is specified the slide is shown in all domains by default.", + "fieldname": "domains", + "fieldtype": "Table", + "label": "Domains", + "options": "Has Domain" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, + { + "description": "Add a help video link just in case user has no idea about what to fill in the slide.", + "fieldname": "help_links", + "fieldtype": "Table", + "label": "Help Links", + "options": "Setup Wizard Help Link" + }, + { + "fieldname": "action_section_break", + "fieldtype": "Section Break", + "label": "Action Settings" + }, + { + "description": "If slide type is Action there should be a submit method bound to be executed after the slide is completed.", + "fieldname": "slide_type", + "fieldtype": "Select", + "label": "Slide Type", + "options": "Information\nCreate\nSettings", + "reqd": 1 + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fieldname": "app", + "fieldtype": "Select", + "label": "App", + "options": "Frappe\nERPNext", + "reqd": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "image_src", + "fieldtype": "Data", + "label": "Slide Image Source" + }, + { + "fieldname": "description_section_break", + "fieldtype": "Section Break", + "label": "Description" + }, + { + "depends_on": "eval:doc.slide_type!='Information'", + "fieldname": "ref_doctype", + "fieldtype": "Link", + "label": "Reference Document Type", + "options": "DocType" + }, + { + "default": "0", + "description": "Determines the order of the slide in the wizard. If the slide is not to be displayed, priority should be set to 0.", + "fieldname": "slide_order", + "fieldtype": "Int", + "label": "Slide Order" + }, + { + "depends_on": "eval:doc.slide_type=='Information'", + "fieldname": "slide_module", + "fieldtype": "Link", + "label": "Module", + "options": "Module Def" + }, + { + "fieldname": "section_break_18", + "fieldtype": "Section Break", + "label": "Fields" + } + ], + "modified": "2019-11-26 17:33:34.553367", + "modified_by": "Administrator", + "module": "Desk", + "name": "Setup Wizard Slide", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.py b/frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.py new file mode 100644 index 0000000000..9109d52a85 --- /dev/null +++ b/frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import json +from frappe.model.document import Document +from frappe.modules.export_file import export_to_files + +class SetupWizardSlide(Document): + def on_update(self): + if self.ref_doctype: + module = frappe.db.get_value('DocType', self.ref_doctype, 'module') + else: + module = self.slide_module + export_to_files(record_list=[['Setup Wizard Slide', self.name]], record_module=module) + +def get_onboarding_slides_as_list(): + slides = [] + slide_docs = frappe.get_all('Setup Wizard Slide', + filters={'slide_order': ('!=', 0)}, + order_by='slide_order') + for entry in slide_docs: + # using get_doc because child table fields are not fetched in get_all + slide_doc = frappe.get_doc('Setup Wizard Slide', entry.name) + if frappe.scrub(slide_doc.app) in frappe.get_installed_apps(): + slides.append(frappe._dict( + slide_type=slide_doc.slide_type, + title=slide_doc.slide_title, + help=slide_doc.slide_desc, + fields=slide_doc.slide_fields, + help_links=get_help_links(slide_doc), + add_more=slide_doc.add_more_button, + max_count=slide_doc.max_count, + submit_method=slide_doc.submit_method, + image_src=get_slide_image(slide_doc), + ref_doctype=slide_doc.ref_doctype, + app=slide_doc.app + )) + return slides + +@frappe.whitelist() +def get_onboarding_slides(): + slides = [] + slide_list = get_onboarding_slides_as_list() + + active_domains = frappe.get_active_domains() + for slide in slide_list: + if not slide.domains or any(domain in active_domains for domain in slide.domains): + slides.append(slide) + return slides + +def get_help_links(slide_doc): + links=[] + for link in slide_doc.help_links: + links.append({ + 'label': link.label, + 'video_id': link.video_id + }) + return links + +def get_slide_image(slide_doc): + if slide_doc.image_src: + return slide_doc.image_src + return None + +@frappe.whitelist() +def create_onboarding_docs(values, doctype=None, submit_method=None, app=None, slide_type=None): + data = json.loads(values) + if submit_method: + try: + method = frappe.scrub(app) + '.utilities.onboarding_utils.' + submit_method + frappe.call(method, data) + except AttributeError: + create_generic_onboarding_doc(data, doctype, slide_type) + else: + doc = frappe.new_doc(doctype) + if hasattr(doc, 'create_onboarding_docs'): + doc.create_onboarding_docs(data) + else: + create_generic_onboarding_doc(data, doctype, slide_type) + +def create_generic_onboarding_doc(data, doctype, slide_type): + if slide_type == 'Settings': + doc = frappe.get_single(doctype) + for entry in data: + doc.set(entry, data.get(entry)) + doc.save() + + elif slide_type == 'Create': + doc = frappe.new_doc(doctype) + for entry in data: + doc.set(entry, data.get(entry)) + doc.flags.ignore_mandatory = True + doc.flags.ignore_links = True + doc.insert() \ No newline at end of file diff --git a/frappe/desk/doctype/setup_wizard_slide/test_setup_wizard_slide.py b/frappe/desk/doctype/setup_wizard_slide/test_setup_wizard_slide.py new file mode 100644 index 0000000000..58652c4ec2 --- /dev/null +++ b/frappe/desk/doctype/setup_wizard_slide/test_setup_wizard_slide.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 TestSetupWizardSlide(unittest.TestCase): + pass diff --git a/frappe/desk/doctype/setup_wizard_slide_field/__init__.py b/frappe/desk/doctype/setup_wizard_slide_field/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/setup_wizard_slide_field/setup_wizard_slide_field.json b/frappe/desk/doctype/setup_wizard_slide_field/setup_wizard_slide_field.json new file mode 100644 index 0000000000..03d58d842b --- /dev/null +++ b/frappe/desk/doctype/setup_wizard_slide_field/setup_wizard_slide_field.json @@ -0,0 +1,74 @@ +{ + "creation": "2019-11-13 13:35:08.617909", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label", + "fieldtype", + "fieldname", + "align", + "placeholder", + "reqd", + "column_break_4", + "options" + ], + "fields": [ + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label" + }, + { + "fieldname": "fieldtype", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Fieldtype", + "options": "Attach\nAttach Image\nCheck\nCurrency\nData\nDate\nDatetime\nFloat\nHTML\nInt\nRating\nSelect\nLink\nSmall Text\nText\nText Editor\nSection Break\nColumn Break" + }, + { + "fieldname": "fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Fieldname" + }, + { + "fieldname": "options", + "fieldtype": "Text", + "in_list_view": 1, + "label": "Options" + }, + { + "fieldname": "align", + "fieldtype": "Select", + "label": "Align", + "options": "\ncenter\nleft\nright" + }, + { + "fieldname": "placeholder", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Placeholder" + }, + { + "default": "0", + "fieldname": "reqd", + "fieldtype": "Check", + "label": "Mandatory" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + } + ], + "istable": 1, + "modified": "2019-11-25 16:50:53.994656", + "modified_by": "Administrator", + "module": "Desk", + "name": "Setup Wizard Slide Field", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/frappe/desk/doctype/setup_wizard_slide_field/setup_wizard_slide_field.py b/frappe/desk/doctype/setup_wizard_slide_field/setup_wizard_slide_field.py new file mode 100644 index 0000000000..1b880ef916 --- /dev/null +++ b/frappe/desk/doctype/setup_wizard_slide_field/setup_wizard_slide_field.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 SetupWizardSlideField(Document): + pass diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index 84d515050c..b142047059 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 @@ -139,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/desk/reportview.py b/frappe/desk/reportview.py index 937285206e..5db6ae18bf 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -9,8 +9,10 @@ from six.moves import range import frappe.permissions from frappe.model.db_query import DatabaseQuery from frappe import _ -from six import text_type, string_types, StringIO +from six import string_types, StringIO from frappe.core.doctype.access_log.access_log import make_access_log +from frappe.utils import cstr + @frappe.whitelist() @frappe.read_only() @@ -170,11 +172,11 @@ def export_query(): writer = csv.writer(f) for r in data: # encode only unicode type strings and not int, floats etc. - writer.writerow([handle_html(frappe.as_unicode(v)).encode('utf-8') \ + writer.writerow([handle_html(frappe.as_unicode(v)) \ if isinstance(v, string_types) else v for v in r]) f.seek(0) - frappe.response['result'] = text_type(f.read(), 'utf-8') + frappe.response['result'] = cstr(f.read()) frappe.response['type'] = 'csv' frappe.response['doctype'] = title diff --git a/frappe/desk/user_progress.py b/frappe/desk/user_progress.py deleted file mode 100644 index f62bb2a29d..0000000000 --- a/frappe/desk/user_progress.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -from __future__ import unicode_literals - -import frappe -from frappe.utils import cint - -@frappe.whitelist() -def get_user_progress_slides(): - ''' - Return user progress slides for the desktop (called via `get_user_progress_slides` hook) - ''' - slides = [] - if cint(frappe.db.get_single_value('System Settings', 'setup_complete')): - for fn in frappe.get_hooks('get_user_progress_slides'): - slides += frappe.get_attr(fn)() - - return slides - -@frappe.whitelist() -def update_and_get_user_progress(): - ''' - Return setup progress action states (called via `update_and_get_user_progress` hook) - ''' - states = {} - for fn in frappe.get_hooks('update_and_get_user_progress'): - states.update(frappe.get_attr(fn)()) - - return states 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/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", 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 b8fde57a43..bccad1fec5 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/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/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..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', '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 6c917b8d4d..a50bf9fdaf 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', 'DocType Link') + _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/db_query.py b/frappe/model/db_query.py index b7feee82f4..64e242d6af 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -513,6 +513,8 @@ class DatabaseQuery(object): or not can_be_null or (f.value and f.operator.lower() in ('=', 'like')) or 'ifnull(' in column_name.lower()): + if f.operator.lower() == 'like' and frappe.conf.get('db_type') == 'postgres': + f.operator = 'ilike' condition = '{column_name} {operator} {value}'.format( column_name=column_name, operator=f.operator, value=value) 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..927a56b6b8 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', '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"}.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 hasattr(self, 'links') and 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') @@ -441,9 +479,11 @@ 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"}), + frappe._dict({"fieldname": "links", "options": "DocType Link"}), ] ####### diff --git a/frappe/model/sync.py b/frappe/model/sync.py index 18bf827c5f..989e1fbb99 100644 --- a/frappe/model/sync.py +++ b/frappe/model/sync.py @@ -29,6 +29,8 @@ 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", "doctype_link"), ("core", "role"), ("core", "has_role"), ("core", "doctype"), @@ -41,7 +43,10 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe ("data_migration", "data_migration_mapping_detail"), ("data_migration", "data_migration_mapping"), ("data_migration", "data_migration_plan_mapping"), - ("data_migration", "data_migration_plan")): + ("data_migration", "data_migration_plan"), + ("desk", "setup_wizard_slide_field"), + ("desk", "setup_wizard_help_link"), + ("desk", "setup_wizard_slide")): files.append(os.path.join(frappe.get_app_path("frappe"), d[0], "doctype", d[1], d[1] + ".json")) @@ -70,7 +75,7 @@ def get_doc_files(files, start_path, force=0, sync_everything = False, verbose=F # load in sequence - warning for devs document_types = ['doctype', 'page', 'report', 'dashboard_chart_source', 'print_format', 'website_theme', 'web_form', 'notification', 'print_style', - 'data_migration_mapping', 'data_migration_plan'] + 'data_migration_mapping', 'data_migration_plan', 'setup_wizard_slide'] for doctype in document_types: doctype_path = os.path.join(start_path, doctype) if os.path.exists(doctype_path): diff --git a/frappe/patches.txt b/frappe/patches.txt index 5dbde7ed40..603df8d778 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -9,6 +9,8 @@ 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', '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') @@ -258,3 +260,4 @@ frappe.patches.v12_0.update_auto_repeat_status_and_not_submittable frappe.patches.v12_0.copy_to_parent_for_tags frappe.patches.v12_0.create_notification_settings_for_user frappe.patches.v11_0.make_all_prepared_report_attachments_private #2019-11-26 +execute:frappe.delete_doc("Test Runner") \ No newline at end of file diff --git a/frappe/printing/doctype/letter_head/letter_head.py b/frappe/printing/doctype/letter_head/letter_head.py index 1a70ebcb08..d75b253dd3 100644 --- a/frappe/printing/doctype/letter_head/letter_head.py +++ b/frappe/printing/doctype/letter_head/letter_head.py @@ -4,8 +4,7 @@ from __future__ import unicode_literals import frappe from frappe.utils import is_image - - +from frappe import _ from frappe.model.document import Document class LetterHead(Document): @@ -43,3 +42,16 @@ class LetterHead(Document): # update control panel - so it loads new letter directly frappe.db.set_default("default_letter_head_content", self.content) + + def create_onboarding_docs(self, args): + letterhead = args.get('letterhead') + if letterhead: + try: + frappe.get_doc({ + 'doctype': self.doctype, + 'image': letterhead, + 'letter_head_name': _('Standard'), + 'is_default': 1 + }).insert() + except frappe.NameError: + pass \ No newline at end of file diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json index a5d41ee099..951a863776 100644 --- a/frappe/printing/doctype/print_format/print_format.json +++ b/frappe/printing/doctype/print_format/print_format.json @@ -1,905 +1,223 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 1, - "autoname": "Prompt", - "beta": 0, - "creation": "2013-01-23 19:54:43", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "editable_grid": 0, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fetch_if_empty": 0, - "fieldname": "doc_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "DocType", - "length": 0, - "no_copy": 0, - "options": "DocType", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "module", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Module", - "length": 0, - "no_copy": 0, - "options": "Module Def", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "disabled", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Disabled", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "No", - "fetch_if_empty": 0, - "fieldname": "standard", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Standard", - "length": 0, - "no_copy": 1, - "oldfieldname": "standard", - "oldfieldtype": "Select", - "options": "No\nYes", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "custom_format", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Custom Format", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "custom_format", - "fetch_if_empty": 0, - "fieldname": "section_break_6", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Jinja", - "depends_on": "custom_format", - "description": "", - "fetch_if_empty": 0, - "fieldname": "print_format_type", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Print Format Type", - "length": 0, - "no_copy": 0, - "options": "Jinja\nJS", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, - "fieldname": "raw_printing", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Raw Printing", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:!doc.raw_printing", - "fetch_if_empty": 0, - "fieldname": "html", - "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "HTML", - "length": 0, - "no_copy": 0, - "oldfieldname": "html", - "oldfieldtype": "Text Editor", - "options": "HTML", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "raw_printing", - "description": "Any string-based printer languages can be used. Writing raw commands requires knowledge of the printer's native language provided by the printer manufacturer. Please refer to the developer manual provided by the printer manufacturer on how to write their native commands. These commands are rendered on the server side using the Jinja Templating Language.", - "fetch_if_empty": 0, - "fieldname": "raw_commands", - "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Raw Commands", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:!doc.custom_format", - "fetch_if_empty": 0, - "fieldname": "section_break_9", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Style Settings", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fetch_if_empty": 0, - "fieldname": "align_labels_right", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Align Labels to the Right", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fetch_if_empty": 0, - "fieldname": "show_section_headings", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Show Section Headings", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fetch_if_empty": 0, - "fieldname": "line_breaks", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Show Line Breaks after Sections", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_11", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "default_print_language", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Default Print Language", - "length": 0, - "no_copy": 0, - "options": "Language", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Default", - "depends_on": "eval:!doc.custom_format", - "fetch_if_empty": 0, - "fieldname": "font", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Font", - "length": 0, - "no_copy": 0, - "options": "Default\nArial\nHelvetica\nVerdana\nMonospace", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:!doc.raw_printing", - "fetch_if_empty": 0, - "fieldname": "css_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "css", - "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Custom CSS", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "custom_html_help", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Custom HTML Help", - "length": 0, - "no_copy": 0, - "options": "Notes:
\n\ndata-fieldtype and data-fieldnamevaluesection-breakcolumn-break1. Left align integers
\n\n[data-fieldtype=\"Int\"] .value { text-left: left; }\n\n1. Add border to sections except the last section
\n\n.section-break { padding: 30px 0px; border-bottom: 1px solid #eee; }\n.section-break:last-child { padding-bottom: 0px; border-bottom: 0px; }\n",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "custom_format",
- "fetch_if_empty": 0,
- "fieldname": "section_break_13",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "custom_format",
- "fetch_if_empty": 0,
- "fieldname": "print_format_help",
- "fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Print Format Help",
- "length": 0,
- "no_copy": 0,
- "options": "Print itemsFormats are rendered on the server side using the Jinja Templating Language. All forms have access to the doc object which contains information about the document that is being formatted. You can also access common utilities via the frappe module.
For styling, the Boostrap CSS framework is provided and you can enjoy the full range of classes.
\n<h3>{{ doc.select_print_heading or \"Invoice\" }}</h3>\n<div class=\"row\">\n\t<div class=\"col-md-3 text-right\">Customer Name</div>\n\t<div class=\"col-md-9\">{{ doc.customer_name }}</div>\n</div>\n<div class=\"row\">\n\t<div class=\"col-md-3 text-right\">Date</div>\n\t<div class=\"col-md-9\">{{ doc.get_formatted(\"invoice_date\") }}</div>\n</div>\n<table class=\"table table-bordered\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<th>Sr</th>\n\t\t\t<th>Item Name</th>\n\t\t\t<th>Description</th>\n\t\t\t<th class=\"text-right\">Qty</th>\n\t\t\t<th class=\"text-right\">Rate</th>\n\t\t\t<th class=\"text-right\">Amount</th>\n\t\t</tr>\n\t\t{%- for row in doc.items -%}\n\t\t<tr>\n\t\t\t<td style=\"width: 3%;\">{{ row.idx }}</td>\n\t\t\t<td style=\"width: 20%;\">\n\t\t\t\t{{ row.item_name }}\n\t\t\t\t{% if row.item_code != row.item_name -%}\n\t\t\t\t<br>Item Code: {{ row.item_code}}\n\t\t\t\t{%- endif %}\n\t\t\t</td>\n\t\t\t<td style=\"width: 37%;\">\n\t\t\t\t<div style=\"border: 0px;\">{{ row.description }}</div></td>\n\t\t\t<td style=\"width: 10%; text-align: right;\">{{ row.qty }} {{ row.uom or row.stock_uom }}</td>\n\t\t\t<td style=\"width: 15%; text-align: right;\">{{\n\t\t\t\trow.get_formatted(\"rate\", doc) }}</td>\n\t\t\t<td style=\"width: 15%; text-align: right;\">{{\n\t\t\t\trow.get_formatted(\"amount\", doc) }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\ndoc.get_formatted(\"[fieldname]\", [parent_doc]) | \n\t\t\tGet document value formatted as Date, Currency etc. Pass parent doc for curreny type fields. | \n\t\t
frappe.db.get_value(\"[doctype]\", \"[name]\", \"fieldname\") | \n\t\t\tGet value from another document. | \n\t\t
Notes:
\n\ndata-fieldtype and data-fieldnamevaluesection-breakcolumn-break1. Left align integers
\n\n[data-fieldtype=\"Int\"] .value { text-left: left; }\n\n1. Add border to sections except the last section
\n\n.section-break { padding: 30px 0px; border-bottom: 1px solid #eee; }\n.section-break:last-child { padding-bottom: 0px; border-bottom: 0px; }\n"
+ },
+ {
+ "depends_on": "custom_format",
+ "fieldname": "section_break_13",
+ "fieldtype": "Section Break"
+ },
+ {
+ "depends_on": "custom_format",
+ "fieldname": "print_format_help",
+ "fieldtype": "HTML",
+ "label": "Print Format Help",
+ "options": "Print itemsFormats are rendered on the server side using the Jinja Templating Language. All forms have access to the doc object which contains information about the document that is being formatted. You can also access common utilities via the frappe module.
For styling, the Boostrap CSS framework is provided and you can enjoy the full range of classes.
\n<h3>{{ doc.select_print_heading or \"Invoice\" }}</h3>\n<div class=\"row\">\n\t<div class=\"col-md-3 text-right\">Customer Name</div>\n\t<div class=\"col-md-9\">{{ doc.customer_name }}</div>\n</div>\n<div class=\"row\">\n\t<div class=\"col-md-3 text-right\">Date</div>\n\t<div class=\"col-md-9\">{{ doc.get_formatted(\"invoice_date\") }}</div>\n</div>\n<table class=\"table table-bordered\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<th>Sr</th>\n\t\t\t<th>Item Name</th>\n\t\t\t<th>Description</th>\n\t\t\t<th class=\"text-right\">Qty</th>\n\t\t\t<th class=\"text-right\">Rate</th>\n\t\t\t<th class=\"text-right\">Amount</th>\n\t\t</tr>\n\t\t{%- for row in doc.items -%}\n\t\t<tr>\n\t\t\t<td style=\"width: 3%;\">{{ row.idx }}</td>\n\t\t\t<td style=\"width: 20%;\">\n\t\t\t\t{{ row.item_name }}\n\t\t\t\t{% if row.item_code != row.item_name -%}\n\t\t\t\t<br>Item Code: {{ row.item_code}}\n\t\t\t\t{%- endif %}\n\t\t\t</td>\n\t\t\t<td style=\"width: 37%;\">\n\t\t\t\t<div style=\"border: 0px;\">{{ row.description }}</div></td>\n\t\t\t<td style=\"width: 10%; text-align: right;\">{{ row.qty }} {{ row.uom or row.stock_uom }}</td>\n\t\t\t<td style=\"width: 15%; text-align: right;\">{{\n\t\t\t\trow.get_formatted(\"rate\", doc) }}</td>\n\t\t\t<td style=\"width: 15%; text-align: right;\">{{\n\t\t\t\trow.get_formatted(\"amount\", doc) }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\ndoc.get_formatted(\"[fieldname]\", [parent_doc]) | \n\t\t\tGet document value formatted as Date, Currency etc. Pass parent doc for curreny type fields. | \n\t\t
frappe.db.get_value(\"[doctype]\", \"[name]\", \"fieldname\") | \n\t\t\tGet value from another document. | \n\t\t
${__('No {0} found', [__(this.doctype)])}
+${no_result_message}
${new_button} + ${help_link}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, """URL: {url}
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 +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(check_time=check_time): + # ensure last job is one day old + last_job_timestamp = frappe.db.get_last_created('Scheduled Job Log') + if not last_job_timestamp: + return True else: - is_dormant = frappe.conf.get('dormant') - if is_dormant: - update_site_config('dormant', 'None') + 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: + # schedulers run in the last 24 hours, do nothing + return False + else: + # site active, lets run the jobs + return True - -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 +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_timestamp: + return True + if ((check_time or now_datetime()) - last_activity_log_timestamp).total_seconds() >= since: return True return False -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] @frappe.whitelist() def activate_scheduler(): diff --git a/frappe/utils/selenium_testdriver.py b/frappe/utils/selenium_testdriver.py deleted file mode 100644 index cde0e5cb45..0000000000 --- a/frappe/utils/selenium_testdriver.py +++ /dev/null @@ -1,299 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -from __future__ import unicode_literals, print_function - -from selenium import webdriver -from selenium.webdriver.common.by import By -from selenium.webdriver.support.ui import WebDriverWait -#from selenium.webdriver.support.select import Select -from selenium.webdriver.support import expected_conditions as EC -#from selenium.common.exceptions import TimeoutException -from selenium.webdriver.chrome.options import Options -from selenium.webdriver.common.desired_capabilities import DesiredCapabilities - -import time -import signal -import os, sys -import frappe -from ast import literal_eval - -class TestDriver(object): - def __init__(self, port=None): - self.port = port or frappe.get_site_config().webserver_port or '8000' - - chrome_options = Options() - capabilities = DesiredCapabilities.CHROME - - if os.environ.get('CI'): - self.host = 'localhost' - else: - self.host = frappe.local.site - - # enable browser logging - capabilities['loggingPrefs'] = {'browser':'ALL'} - - chrome_options.add_argument('--no-sandbox') - chrome_options.add_argument('--start-maximized') - self.driver = webdriver.Chrome(chrome_options=chrome_options, - desired_capabilities=capabilities, port=9515) - - # self.driver.set_window_size(1080,800) - self.cur_route = None - self.logged_in = False - - @property - def localhost(self): - return "http://{host}:{port}".format(host=self.host, port=self.port) - - def get(self, url): - return self.driver.get(os.path.join(self.localhost, url)) - - def start(self): - def signal_handler(signal, frame): - self.close() - sys.exit(0) - signal.signal(signal.SIGINT, signal_handler) - - def refresh(self): - self.driver.refresh() - - def close(self): - if self.driver: - self.driver.quit() - self.driver = None - - def login(self, wait_for_id="#page-desktop", animate=0, scroll_offset=0): - if self.logged_in: - return - self.get('login') - self.wait_for("#login_email") - self.set_input("#login_email", "Administrator") - self.set_input("#login_password", "admin") - self.click('.btn-login', animate=animate, offset=scroll_offset) - self.wait_for(wait_for_id) - self.logged_in = True - - def set_input(self, selector, text, key=None, xpath=None): - elem = self.find(selector, xpath=xpath)[0] - elem.clear() - elem.send_keys(text) - if key: - time.sleep(0.5) - elem.send_keys(key) - time.sleep(0.2) - - def set_field(self, fieldname, text): - elem = self.wait_for(xpath='//input[@data-fieldname="{0}"]'.format(fieldname)) - time.sleep(0.2) - elem.send_keys(text) - - def set_select(self, fieldname, text): - elem = self.wait_for(xpath='//select[@data-fieldname="{0}"]'.format(fieldname)) - time.sleep(0.2) - elem.send_keys(text) - - def set_multicheck(self, fieldname, values): - for value in values: - path = '//div[@data-fieldname="{0}"]//span[@data-unit="{1}"]'.format(fieldname, value) - elem = self.wait_for(xpath=path) - time.sleep(0.2) - elem.click() - - def set_text_editor(self, fieldname, text): - elem = self.wait_for(xpath='//div[@data-fieldname="{0}"]//div[@contenteditable="true"]'.format(fieldname)) - time.sleep(0.2) - elem.send_keys(text) - - def find(self, selector=None, everywhere=False, xpath=None): - if xpath: - return self.driver.find_elements_by_xpath(xpath) - else: - if self.cur_route and not everywhere: - selector = self.cur_route + " " + selector - return self.driver.find_elements_by_css_selector(selector) - - def wait_for(self, selector=None, everywhere=False, timeout=20, xpath=None, for_invisible=False): - if self.cur_route and not everywhere: - selector = self.cur_route + " " + selector - - time.sleep(0.5) - - if selector: - _by = By.CSS_SELECTOR - if xpath: - _by = By.XPATH - selector = xpath - - try: - if not for_invisible: - elem = self.get_wait(timeout).until( - EC.presence_of_element_located((_by, selector))) - else: - elem = self.get_wait(timeout).until( - EC.invisibility_of_element_located((_by, selector))) - return elem - except Exception as e: - # body = self.driver.find_element_by_id('body_div') - # print(body.get_attribute('innerHTML')) - self.print_console() - raise e - - def wait_for_invisible(self, selector=None, everywhere=False, timeout=20, xpath=None): - self.wait_for(selector, everywhere, timeout, xpath, True) - - def get_console(self): - out = [] - for entry in self.driver.get_log('browser'): - source, line_no, message = entry.get('message').split(' ', 2) - - if message and message[0] in ('"', "'"): - # message is a quoted/escaped string - message = literal_eval(message) - - out.append(source + ' ' + line_no) - out.append(message) - out.append('-'*40) - - return out - - def print_console(self): - for line in self.get_console(): - print(line) - - def get_wait(self, timeout=20): - return WebDriverWait(self.driver, timeout) - - def scroll_to(self, selector, animate=0, offset=0): - self.execute_script("frappe.ui.scroll('{0}', {1}, {2})".format(selector, animate, offset)) - - def set_route(self, *args): - self.execute_script('frappe.set_route({0})'\ - .format(', '.join(['"{0}"'.format(r) for r in args]))) - - self.wait_for(xpath='//div[@data-page-route="{0}"]'.format('/'.join(args)), timeout=4) - - def click(self, css_selector, xpath=None, timeout=20, animate=0, offset=0): - element = self.wait_till_clickable(css_selector, xpath, timeout) - self.scroll_to(css_selector, animate, offset) - time.sleep(0.5) - element.click() - return element - - def click_primary_action(self): - selector = ".page-actions .primary-action" - #self.scroll_to(selector) - self.wait_till_clickable(selector).click() - self.wait_for_ajax() - - def click_secondary_action(self): - selector = ".page-actions .btn-secondary" - #self.scroll_to(selector) - self.wait_till_clickable(selector).click() - self.wait_for_ajax() - - def click_modal_primary_action(self): - self.get_visible_modal().find_element_by_css_selector('.btn-primary').click() - - def get_visible_modal(self): - return self.get_visible_element('.modal-content') - - def get_visible_element(self, selector=None, xpath=None): - for elem in self.find(selector=selector, xpath=xpath): - if elem.is_displayed(): - return elem - - def wait_till_clickable(self, selector=None, xpath=None, timeout=20): - if self.cur_route: - selector = self.cur_route + " " + selector - - by = By.CSS_SELECTOR - if xpath: - by = By.XPATH - selector = xpath - - return self.get_wait(timeout).until(EC.element_to_be_clickable( - (by, selector))) - - - def execute_script(self, js): - self.driver.execute_script(js) - - def wait_for_ajax(self, freeze = False): - self.wait_for('body[data-ajax-state="complete"]', True) - if freeze: - self.wait_for_invisible(".freeze-message-container") - - -# def go_to_module(module_name, item=None): -# global cur_route -# -# # desktop -# find(".navbar-home", True)[0].click() -# cur_route = None -# wait("#page-desktop") -# -# page = "Module/" + module_name -# m = find('#page-desktop [data-link="{0}"] .app-icon'.format(page)) -# if not m: -# page = "List/" + module_name -# m = find('#page-desktop [data-link="{0}"] .app-icon'.format(page)) -# if not m: -# raise Exception("Module {0} not found".format(module_name)) -# -# m[0].click() -# wait_for_page(page) -# -# if item: -# elem = find('[data-label="{0}"]'.format(item))[0] -# elem.click() -# page = elem.get_attribute("data-route") -# wait_for_page(page) -# -# def new_doc(module, doctype): -# go_to_module(module, doctype) -# primary_action() -# wait_for_page("Form/" + doctype) -# -# def add_child(fieldname): -# find('[data-fieldname="{0}"] .grid-add-row'.format(fieldname))[0].click() -# wait('[data-fieldname="{0}"] .form-grid'.format(fieldname)) -# -# def done_add_child(fieldname): -# selector = '[data-fieldname="{0}"] .grid-row-open .btn-success'.format(fieldname) -# scroll_to(selector) -# wait_till_clickable(selector).click() -# -# def set_field(fieldname, value, fieldtype="input"): -# _driver.switch_to.window(_driver.current_window_handle) -# selector = '{0}[data-fieldname="{1}"]'.format(fieldtype, fieldname) -# set_input(selector, value, key=Keys.TAB) -# wait_for_ajax() -# -# def set_select(fieldname, value): -# select = Select(find('select[data-fieldname="{0}"]'.format(fieldname))[0]) -# select.select_by_value(value) -# wait_for_ajax() -# -# -# def wait_for_page(name): -# global cur_route -# cur_route = None -# route = '[data-page-route="{0}"]'.format(name) -# wait_for_ajax() -# elem = wait(route) -# wait_for_ajax() -# cur_route = route -# return elem -# -# -# def wait_till_visible(selector): -# if cur_route: -# selector = cur_route + " " + selector -# return get_wait().until(EC.visibility_of_element_located((By.CSS_SELECTOR, selector))) -# -# -# def wait_for_state(state): -# return wait(cur_route + '[data-state="{0}"]'.format(state), True) -# -# diff --git a/frappe/website/doctype/blog_post/test_blog_post.py b/frappe/website/doctype/blog_post/test_blog_post.py index 8278125723..aecc813e9d 100644 --- a/frappe/website/doctype/blog_post/test_blog_post.py +++ b/frappe/website/doctype/blog_post/test_blog_post.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.tests.test_website import set_request +from frappe.utils import set_request from frappe.website.render import render from frappe.utils import random_string diff --git a/frappe/website/doctype/web_page/test_web_page.py b/frappe/website/doctype/web_page/test_web_page.py index 38daebe5e8..b1600a338f 100644 --- a/frappe/website/doctype/web_page/test_web_page.py +++ b/frappe/website/doctype/web_page/test_web_page.py @@ -3,7 +3,7 @@ import unittest import frappe from frappe.website.router import resolve_route import frappe.website.render -from frappe.tests import set_request +from frappe.utils import set_request test_records = frappe.get_test_records('Web Page') diff --git a/frappe/website/doctype/website_route_meta/test_website_route_meta.py b/frappe/website/doctype/website_route_meta/test_website_route_meta.py index 90e40972ef..c02dc398bf 100644 --- a/frappe/website/doctype/website_route_meta/test_website_route_meta.py +++ b/frappe/website/doctype/website_route_meta/test_website_route_meta.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.tests.test_website import set_request +from frappe.utils import set_request from frappe.website.render import render class TestWebsiteRouteMeta(unittest.TestCase): diff --git a/frappe/website/doctype/website_route_meta/website_route_meta.js b/frappe/website/doctype/website_route_meta/website_route_meta.js index f13963e14f..0e41e92b8e 100644 --- a/frappe/website/doctype/website_route_meta/website_route_meta.js +++ b/frappe/website/doctype/website_route_meta/website_route_meta.js @@ -4,5 +4,12 @@ frappe.ui.form.on('Website Route Meta', { refresh: function(frm) { frm.get_field('__newname').set_label('Route'); + frm.add_custom_button(__('Visit Web Page'), () => { + let route = frm.doc.name; + if (!route.startsWith('/')) { + route = '/' + route; + } + window.open(route, '_blank'); + }); } }); diff --git a/frappe/website/doctype/website_route_meta/website_route_meta.json b/frappe/website/doctype/website_route_meta/website_route_meta.json index 1ca6a70fa0..6e1672c379 100644 --- a/frappe/website/doctype/website_route_meta/website_route_meta.json +++ b/frappe/website/doctype/website_route_meta/website_route_meta.json @@ -1,97 +1,43 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, + "allow_rename": 1, "autoname": "Prompt", - "beta": 0, "creation": "2019-02-16 17:37:27.918909", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "meta_tags" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "meta_tags", "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Meta Tags", - "length": 0, - "no_copy": 0, "options": "Website Meta Tag", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-02-16 17:37:27.918909", + "modified": "2019-11-28 17:26:49.068372", "modified_by": "Administrator", "module": "Website", "name": "Website Route Meta", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} \ No newline at end of file + "track_changes": 1 +} diff --git a/requirements.txt b/requirements.txt index 2db499d53f..96e57fd1f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,68 +1,67 @@ -boto3 -chardet -dropbox==9.1.0 -gunicorn -jinja2 -markdown2==2.3.6 -PyMySQL -maxminddb-geolite2 -python-dateutil -pytz -six -werkzeug -semantic_version -rauth>=0.6.2 -requests -redis==2.10.6 -selenium -babel==2.6.0 -ipython -html2text==2016.9.19 -email_reply_parser -click==7.0 -num2words==0.5.5 -watchdog==0.8.0 +Babel==2.6.0 +beautifulsoup4==4.8.1 +bleach-whitelist==0.0.10 bleach==2.1.4 -bleach-whitelist -Pillow -beautifulsoup4 -rq==0.12.0 -schedule -cryptography -pyopenssl -ndg-httpsclient -pyasn1 -zxcvbn-python -unittest-xml-reporting -oauthlib -requests-oauthlib -pdfkit -PyJWT -PyPDF2 -openpyxl -pyotp -pyqrcode -pypng -premailer -croniter -googlemaps==3.1.1 -braintree -future -passlib -google-api-python-client -google-auth -google-auth-httplib2 -google-auth-oauthlib -faker -stripe -coverage -urllib3 +boto3==1.10.18 +braintree==3.57.1 +chardet==3.0.4 +Click==7.0 +coverage==4.5.4 +croniter==0.3.30 +cryptography==2.8 +dropbox==9.1.0 +email-reply-parser==0.5.9 +Faker==2.0.4 +frontmatter==3.0.5 +future==0.18.2 GitPython==2.1.11 -psycopg2==2.7.5 +google-api-python-client==1.7.11 +google-auth-httplib2==0.0.3 +google-auth-oauthlib==0.4.1 +google-auth==1.7.1 +googlemaps==3.1.1 +gunicorn==19.9.0 +html2text==2016.9.19 +ipython==5.8.0 +Jinja2==2.10.3 +markdown2==2.3.6 +maxminddb-geolite2==2018.703 +ndg-httpsclient==0.5.1 +num2words==0.5.5 +oauthlib==3.1.0 +openpyxl==2.6.4 +passlib==1.7.1 +pdfkit==0.6.1 +Pillow==6.2.1 +premailer==3.6.1 psycopg2-binary==2.7.5 -sqlparse==0.2.4 +psycopg2==2.7.5 +pyasn1==0.4.7 Pygments==2.2.0 -frontmatter +PyJWT==1.7.1 +PyMySQL==0.9.3 +pyOpenSSL==19.0.0 +pyotp==2.3.0 +PyPDF2==1.26.0 +pypng==0.0.20 +PyQRCode==1.2.1 +python-dateutil==2.8.1 +pytz==2019.3 PyYAML==3.13 -xlrd -RestrictedPython==5.0 \ No newline at end of file +rauth==0.7.3 +redis>=3.0 +requests-oauthlib==1.3.0 +requests==2.22.0 +RestrictedPython==5.0 +rq>=1.1.0 +schedule==0.6.0 +semantic-version==2.8.2 +six==1.13.0 +sqlparse==0.2.4 +stripe==2.40.0 +unittest-xml-reporting==2.5.2 +urllib3==1.25.7 +watchdog==0.8.0 +Werkzeug==0.16.0 +xlrd==1.2.0 +zxcvbn-python==4.4.24 \ No newline at end of file