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": "

Custom CSS Help

\n\n

Notes:

\n\n
    \n
  1. All field groups (label + value) are set attributes data-fieldtype and data-fieldname
  2. \n
  3. All values are given class value
  4. \n
  5. All Section Breaks are given class section-break
  6. \n
  7. All Column Breaks are given class column-break
  8. \n
\n\n

Examples

\n\n

1. Left align integers

\n\n
[data-fieldtype=\"Int\"] .value { text-left: left; }
\n\n

1. 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 Format Help

\n
\n

Introduction

\n

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.

\n

For styling, the Boostrap CSS framework is provided and you can enjoy the full range of classes.

\n
\n

References

\n
    \n\t
  1. Jinja Tempalting Language: Reference
  2. \n\t
  3. Bootstrap CSS Framework
  4. \n
\n
\n

Example

\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>
\n
\n

Common Functions

\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n
doc.get_formatted(\"[fieldname]\", [parent_doc])Get document value formatted as Date, Currency etc. Pass parent doc for curreny type fields.
frappe.db.get_value(\"[doctype]\", \"[name]\", \"fieldname\")Get value from another document.
\n", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "format_data", - "fieldtype": "Code", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Format Data", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "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": "print_format_builder", - "fieldtype": "Check", - "hidden": 1, - "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 Builder", - "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 - } - ], - "has_web_view": 0, - "hide_toolbar": 0, - "icon": "fa fa-print", - "idx": 1, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2019-06-05 12:45:25.869180", - "modified_by": "Administrator", - "module": "Printing", - "name": "Print Format", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 0, - "read_only": 0, - "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 + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2013-01-23 19:54:43", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "doc_type", + "module", + "disabled", + "column_break_3", + "standard", + "custom_format", + "section_break_6", + "print_format_type", + "raw_printing", + "html", + "raw_commands", + "section_break_9", + "align_labels_right", + "show_section_headings", + "line_breaks", + "column_break_11", + "default_print_language", + "font", + "css_section", + "css", + "custom_html_help", + "section_break_13", + "print_format_help", + "format_data", + "print_format_builder" + ], + "fields": [ + { + "fieldname": "doc_type", + "fieldtype": "Link", + "in_filter": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "DocType", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "module", + "fieldtype": "Link", + "label": "Module", + "options": "Module Def" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "default": "No", + "fieldname": "standard", + "fieldtype": "Select", + "in_filter": 1, + "label": "Standard", + "no_copy": 1, + "oldfieldname": "standard", + "oldfieldtype": "Select", + "options": "No\nYes", + "reqd": 1, + "search_index": 1 + }, + { + "default": "0", + "fieldname": "custom_format", + "fieldtype": "Check", + "label": "Custom Format" + }, + { + "depends_on": "custom_format", + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "default": "Jinja", + "depends_on": "custom_format", + "fieldname": "print_format_type", + "fieldtype": "Select", + "label": "Print Format Type", + "options": "Jinja\nJS" + }, + { + "default": "0", + "fieldname": "raw_printing", + "fieldtype": "Check", + "label": "Raw Printing" + }, + { + "depends_on": "eval:!doc.raw_printing", + "fieldname": "html", + "fieldtype": "Code", + "label": "HTML", + "oldfieldname": "html", + "oldfieldtype": "Text Editor", + "options": "HTML" + }, + { + "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.", + "fieldname": "raw_commands", + "fieldtype": "Code", + "label": "Raw Commands" + }, + { + "depends_on": "eval:!doc.custom_format", + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "label": "Style Settings" + }, + { + "default": "0", + "fieldname": "align_labels_right", + "fieldtype": "Check", + "label": "Align Labels to the Right" + }, + { + "default": "0", + "fieldname": "show_section_headings", + "fieldtype": "Check", + "label": "Show Section Headings" + }, + { + "default": "0", + "fieldname": "line_breaks", + "fieldtype": "Check", + "label": "Show Line Breaks after Sections" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "default_print_language", + "fieldtype": "Link", + "label": "Default Print Language", + "options": "Language" + }, + { + "default": "Default", + "depends_on": "eval:!doc.custom_format", + "fieldname": "font", + "fieldtype": "Select", + "label": "Font", + "options": "Default\nArial\nHelvetica\nVerdana\nMonospace" + }, + { + "depends_on": "eval:!doc.raw_printing", + "fieldname": "css_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "css", + "fieldtype": "Code", + "label": "Custom CSS", + "options": "CSS" + }, + { + "fieldname": "custom_html_help", + "fieldtype": "HTML", + "label": "Custom HTML Help", + "options": "

Custom CSS Help

\n\n

Notes:

\n\n
    \n
  1. All field groups (label + value) are set attributes data-fieldtype and data-fieldname
  2. \n
  3. All values are given class value
  4. \n
  5. All Section Breaks are given class section-break
  6. \n
  7. All Column Breaks are given class column-break
  8. \n
\n\n

Examples

\n\n

1. Left align integers

\n\n
[data-fieldtype=\"Int\"] .value { text-left: left; }
\n\n

1. 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 Format Help

\n
\n

Introduction

\n

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.

\n

For styling, the Boostrap CSS framework is provided and you can enjoy the full range of classes.

\n
\n

References

\n
    \n\t
  1. Jinja Tempalting Language: Reference
  2. \n\t
  3. Bootstrap CSS Framework
  4. \n
\n
\n

Example

\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>
\n
\n

Common Functions

\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n
doc.get_formatted(\"[fieldname]\", [parent_doc])Get document value formatted as Date, Currency etc. Pass parent doc for curreny type fields.
frappe.db.get_value(\"[doctype]\", \"[name]\", \"fieldname\")Get value from another document.
\n" + }, + { + "fieldname": "format_data", + "fieldtype": "Code", + "hidden": 1, + "label": "Format Data" + }, + { + "default": "0", + "fieldname": "print_format_builder", + "fieldtype": "Check", + "hidden": 1, + "label": "Print Format Builder" + } + ], + "icon": "fa fa-print", + "idx": 1, + "modified": "2019-11-28 12:40:40.364699", + "modified_by": "faris@erpnext.com", + "module": "Printing", + "name": "Print Format", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 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/printing/page/print_format_builder/print_format_builder.js b/frappe/printing/page/print_format_builder/print_format_builder.js index 8605fb762e..f9bb524d54 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder.js +++ b/frappe/printing/page/print_format_builder/print_format_builder.js @@ -695,7 +695,8 @@ frappe.PrintFormatBuilder = Class.extend({ { fieldname: "content", fieldtype: "Code", - label: label + label: label, + options: "HTML" }, { fieldname: "help", diff --git a/frappe/printing/setup_wizard_slide/company_letter_head/company_letter_head.json b/frappe/printing/setup_wizard_slide/company_letter_head/company_letter_head.json new file mode 100644 index 0000000000..3644a54089 --- /dev/null +++ b/frappe/printing/setup_wizard_slide/company_letter_head/company_letter_head.json @@ -0,0 +1,37 @@ +{ + "add_more_button": 0, + "app": "ERPNext", + "creation": "2019-11-22 13:25:42.892593", + "docstatus": 0, + "doctype": "Setup Wizard Slide", + "domains": [], + "help_links": [ + { + "label": "Know more about printing and branding through letterhead", + "video_id": "cKZHcx1znMc" + } + ], + "idx": 0, + "image_src": "/assets/erpnext/images/illustrations/letterhead.png", + "max_count": 0, + "modified": "2019-11-27 11:39:56.213373", + "modified_by": "Administrator", + "name": "Company Letter Head", + "owner": "Administrator", + "ref_doctype": "Letter Head", + "slide_desc": "Attach Letterhead: (Keep it web friendly as 1024px by 128px)", + "slide_fields": [ + { + "align": "center", + "fieldname": "letterhead", + "fieldtype": "Attach Image", + "label": "Attach Letterhead", + "options": "image", + "reqd": 0 + } + ], + "slide_order": 20, + "slide_title": "Company Letter Head", + "slide_type": "Create", + "submit_method": "" +} \ No newline at end of file diff --git a/frappe/public/build.json b/frappe/public/build.json index 32a2cfd223..76d6d74563 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -148,6 +148,7 @@ "public/js/frappe/ui/page.html", "public/js/frappe/ui/page.js", "public/js/frappe/ui/slides.js", + "public/js/frappe/ui/onboarding_dialog.js", "public/js/frappe/ui/find.js", "public/js/frappe/ui/iconbar.js", "public/js/frappe/form/layout.js", diff --git a/frappe/public/css/desk.css b/frappe/public/css/desk.css index 8e43be12ea..6ddf93df6a 100644 --- a/frappe/public/css/desk.css +++ b/frappe/public/css/desk.css @@ -1158,16 +1158,6 @@ input[type="checkbox"]:focus { color: #fff; border-color: #b1bdca; } -.user-progress-dialog .slides-progress { +.onboarding-dialog .slides-progress { margin-top: 15px; } -.user-progress-dialog .done-state .check-container { - font-size: 64px; - margin: 40px; -} -.user-progress-dialog .done-state .title { - font-weight: normal; -} -.user-progress-dialog .done-state .help-links a { - margin: 0px 10px; -} diff --git a/frappe/public/images/ui-states/empty.png b/frappe/public/images/ui-states/empty.png new file mode 100644 index 0000000000..de2a893508 Binary files /dev/null and b/frappe/public/images/ui-states/empty.png differ diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 4fbea6684f..206094dd71 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -88,6 +88,9 @@ frappe.Application = Class.extend({ } this.show_update_available(); + if (frappe.boot.is_first_startup) { + this.setup_onboarding_wizard(); + } if(frappe.ui.startup_setup_dialog && !frappe.boot.setup_complete) { frappe.ui.startup_setup_dialog.pre_show(); @@ -482,6 +485,28 @@ frappe.Application = Class.extend({ }); }, + setup_onboarding_wizard: () => { + var me = this; + frappe.call('frappe.desk.doctype.setup_wizard_slide.setup_wizard_slide.get_onboarding_slides').then(res => { + if (res.message) { + let slides = res.message; + if (slides.length) { + frappe.require("assets/frappe/js/frappe/ui/onboarding_dialog.js", () => { + me.progress_dialog = new frappe.setup.OnboardingDialog({ + slides: slides + }); + me.progress_dialog.show(); + frappe.call({ + method: "frappe.desk.page.setup_wizard.setup_wizard.reset_is_first_startup", + args: {}, + callback: () => {} + }); + }); + } + } + }); + }, + setup_analytics: function() { if(window.mixpanel) { window.mixpanel.identify(frappe.session.user); diff --git a/frappe/public/js/frappe/form/controls/currency.js b/frappe/public/js/frappe/form/controls/currency.js index 196ae5fd43..4d6b37aede 100644 --- a/frappe/public/js/frappe/form/controls/currency.js +++ b/frappe/public/js/frappe/form/controls/currency.js @@ -1,21 +1,6 @@ frappe.ui.form.ControlCurrency = frappe.ui.form.ControlFloat.extend({ - eval_expression: function(value) { - if (typeof value ==='string' && value.match(/^[0-9+-/* ]+$/)) { - // Removes seperator - value = strip_number_groups(value, this.get_number_format()); - - try { - return eval(value); - } catch (e) { - return value; - } - } - // If not string - return value; - }, - format_for_input: function(value) { - var formatted_value = format_number(parseFloat(value), this.get_number_format(), this.get_precision()); + var formatted_value = format_number(value, this.get_number_format(), this.get_precision()); return isNaN(parseFloat(value)) ? "" : formatted_value; }, diff --git a/frappe/public/js/frappe/form/controls/float.js b/frappe/public/js/frappe/form/controls/float.js index d32f29e28a..308d970f6e 100644 --- a/frappe/public/js/frappe/form/controls/float.js +++ b/frappe/public/js/frappe/form/controls/float.js @@ -1,7 +1,7 @@ frappe.ui.form.ControlFloat = frappe.ui.form.ControlInt.extend({ parse: function(value) { value = this.eval_expression(value); - return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision(), + return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision(), // While parsing currency, get_number_format passes currency's number_format // In case of parsing float, it passes global number_format this.get_number_format()); @@ -12,7 +12,7 @@ frappe.ui.form.ControlFloat = frappe.ui.form.ControlInt.extend({ if (this.df.fieldtype==="Float" && this.df.options && this.df.options.trim()) { number_format = this.get_number_format(); } - var formatted_value = format_number(parseFloat(value), number_format, this.get_precision()); + var formatted_value = format_number(value, number_format, this.get_precision()); return isNaN(parseFloat(value)) ? "" : formatted_value; }, diff --git a/frappe/public/js/frappe/form/controls/int.js b/frappe/public/js/frappe/form/controls/int.js index ead48a996c..5639e5f132 100644 --- a/frappe/public/js/frappe/form/controls/int.js +++ b/frappe/public/js/frappe/form/controls/int.js @@ -20,20 +20,18 @@ frappe.ui.form.ControlInt = frappe.ui.form.ControlData.extend({ }); }, eval_expression: function(value) { - if (typeof value==='string' - && value.match(/^[0-9+-/* ]+$/) - // strings with commas are evaluated incorrectly - // for e.g 47,186.00 -> 186 - && !value.includes(',')) { - try { - return eval(value); - } catch (e) { - // bad expression - return value; + if (typeof value === 'string') { + if (value.match(/^[0-9\+\-\/\* ]+$/)) { + // If it is a string containing operators + try { + return eval(value); + } catch (e) { + // bad expression + return value; + } } - } else { - return value; } + return value; }, parse: function(value) { return cint(this.eval_expression(value), null); diff --git a/frappe/public/js/frappe/form/controls/multiselect_list.js b/frappe/public/js/frappe/form/controls/multiselect_list.js index 0313ee70d7..3e8dc21dca 100644 --- a/frappe/public/js/frappe/form/controls/multiselect_list.js +++ b/frappe/public/js/frappe/form/controls/multiselect_list.js @@ -17,6 +17,7 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({ this.$list_wrapper = $(template); this.$input = $(''); + this.input = this.$input.get(0); this.$list_wrapper.prependTo(this.input_area); this.$filter_input = this.$list_wrapper.find('input'); this.$list_wrapper.on('click', '.dropdown-menu', e => { diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 5a983986d8..0c28b22f35 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -111,6 +111,7 @@ frappe.ui.form.Form = class FrappeForm { $("body").attr("data-sidebar", 1); } this.setup_file_drop(); + this.setup_doctype_actions(); this.setup_done = true; } @@ -319,6 +320,29 @@ frappe.ui.form.Form = class FrappeForm { } } + // sets up the refresh event for custom buttons + // added via configuration + setup_doctype_actions() { + if (this.meta.actions) { + for (let action of this.meta.actions) { + frappe.ui.form.on(this.doctype, 'refresh', () => { + if (!this.is_new()) { + this.add_custom_button(action.label, () => { + if (action.action_type==='Server Action') { + frappe.xcall(action.action, {doc: this.doc}).then(() => { + frappe.msgprint({ + message: __('{} Complete', [action.label]), + alert: true + }); + }); + } + }, action.group); + } + }); + } + } + } + switch_doc(docname) { // record switch if(this.docname != docname && (!this.meta.in_dialog || this.in_form) && !this.meta.istable) { diff --git a/frappe/public/js/frappe/form/print.js b/frappe/public/js/frappe/form/print.js index eb9c0240f5..eb1b1464a7 100644 --- a/frappe/public/js/frappe/form/print.js +++ b/frappe/public/js/frappe/form/print.js @@ -43,6 +43,13 @@ frappe.ui.form.PrintPreview = Class.extend({ me.multilingual_preview(); }); + this.wrapper + .find(".print-preview-refresh") + .on("click", function () { + me.set_default_print_language(); + me.multilingual_preview(); + }); + //On selection of language get code and pass it to preview method this.language_sel = this.wrapper .find(".languages") diff --git a/frappe/public/js/frappe/form/templates/print_layout.html b/frappe/public/js/frappe/form/templates/print_layout.html index 9bdba0d99d..568bcd830b 100644 --- a/frappe/public/js/frappe/form/templates/print_layout.html +++ b/frappe/public/js/frappe/form/templates/print_layout.html @@ -1,7 +1,13 @@
-
+
diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 724f6829c2..777030c923 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -301,15 +301,32 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { this.columns = this.columns.slice(0, column_count); } + get_documentation_link() { + if (this.meta.documentation) { + return `Need Help?`; + } + return ''; + } + get_no_result_message() { + let help_link = this.get_documentation_link(); + let filters = this.filter_area.get(); + let no_result_message = filters.length ? __('No {0} found', [__(this.doctype)]) : __('You haven\'t created a {0} yet', [__(this.doctype)]); + let new_button_label = filters.length ? __('Create a new {0}', [__(this.doctype)]) : __('Create your first {0}', [__(this.doctype)]); + let empty_state_image = this.settings.empty_state_image || '/assets/frappe/images/ui-states/empty.png'; + const new_button = this.can_create ? `

` : ''; return `
-

${__('No {0} found', [__(this.doctype)])}

+
+ Generic Empty State +
+

${no_result_message}

${new_button} + ${help_link}
`; } @@ -391,6 +408,12 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { } after_render() { + this.$no_result.html(` +
+ ${this.get_no_result_message()} +
+ `); + this.setup_new_doc_event(); this.list_sidebar.reload_stats(); } diff --git a/frappe/public/js/frappe/social/pages/UserList.vue b/frappe/public/js/frappe/social/pages/UserList.vue index aff174ea10..69203a8e81 100644 --- a/frappe/public/js/frappe/social/pages/UserList.vue +++ b/frappe/public/js/frappe/social/pages/UserList.vue @@ -47,13 +47,6 @@ @click="toggle_log(user.name)" >{{ user[key] }} -
  • {{__('No user found')}}
  • diff --git a/frappe/public/js/frappe/ui/onboarding_dialog.js b/frappe/public/js/frappe/ui/onboarding_dialog.js new file mode 100644 index 0000000000..1798d0fe68 --- /dev/null +++ b/frappe/public/js/frappe/ui/onboarding_dialog.js @@ -0,0 +1,139 @@ +// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// License: GNU General Public License v3. See license.txt + +frappe.provide("frappe.setup"); +frappe.provide("frappe.ui"); + +frappe.setup.OnboardingSlide = class OnboardingSlide extends frappe.ui.Slide { + constructor(slide = null) { + super(slide); + } + + make() { + super.make(); + this.$next_btn = this.slides_footer.find('.next-btn'); + this.$complete_btn = this.slides_footer.find('.complete-btn'); + this.$action_button = this.slides_footer.find('.next-btn'); + if (this.help_links) { + this.$help_links = $(`
    + +
    `).appendTo(this.$body); + this.setup_help_links(); + } + } + + before_show() { + (this.id === 0) ? + this.$next_btn.text(__('Start')) : this.$next_btn.text(__('Next')); + //last slide + if (this.id === this.parent[0].children.length-1) { + this.$complete_btn.removeClass('hide').addClass('action primary'); + this.$next_btn.removeClass('action primary'); + this.$action_button = this.$complete_btn; + } + this.setup_action_button(); + } + + primary_action() { + let me = this; + if (this.set_values()) { + this.$action_button.addClass('disabled'); + if (me.add_more) me.values.max_count = me.max_count; + frappe.call({ + method: 'frappe.desk.doctype.setup_wizard_slide.setup_wizard_slide.create_onboarding_docs', + args: { + values: me.values, + doctype: me.ref_doctype, + submit_method: me.submit_method, + app: me.app, + slide_type: me.slide_type + }, + callback: function() { + if (me.id === me.parent[0].children.length-1) { + $('.onboarding-dialog').modal('toggle'); + frappe.msgprint({ + message: __('You are all set up!'), + indicator: 'green', + title: __('Success') + }); + } + }, + onerror: function() { + me.slides_footer.find('.primary').removeClass('disabled'); + }, + freeze: true + }); + } + } + + unbind_primary_action() { + // unbind only action method as next button is same as create button in this setup wizard + this.$action_button.off('click.primary_action'); + } + + setup_help_links() { + this.help_links.map(link => { + let $link = $( + `${link.label} + + + ` + ); + if (link.video_id) { + $link.on('click', () => { + frappe.help.show_video(link.video_id, link.label); + }); + } + this.$help_links.append($link); + }); + } + + setup_action_button() { + if (this.slide_type !== 'Information') { + this.$action_button.addClass('primary'); + } else { + this.$action_button.removeClass('primary'); + } + } +}; + +frappe.setup.OnboardingDialog = class OnboardingDialog { + constructor({ + slides = [] + }) { + this.slides = slides; + this.setup(); + } + + setup() { + this.dialog = new frappe.ui.Dialog({ + static: true, + minimizable: false, + title: __("Let's Onboard!") + }); + this.$wrapper = $(this.dialog.$wrapper).addClass('onboarding-dialog'); + this.slide_container = new frappe.ui.Slides({ + parent: this.dialog.body, + slides: this.slides, + slide_class: frappe.setup.OnboardingSlide, + unidirectional: 1, + before_load: ($footer) => { + $footer.find('.prev-btn').addClass('hide'); + $footer.find('.next-btn').removeClass('btn-default').addClass('btn-primary action'); + $footer.find('.text-right').prepend( + $(` + ${__("Complete")}`)); + } + }); + + this.$wrapper.find('.modal-title').prepend( + ` + + ` + ); + } + + show() { + this.dialog.show(); + } +}; diff --git a/frappe/public/js/frappe/ui/slides.js b/frappe/public/js/frappe/ui/slides.js index 7db8e958ce..d2e504337c 100644 --- a/frappe/public/js/frappe/ui/slides.js +++ b/frappe/public/js/frappe/ui/slides.js @@ -26,7 +26,9 @@ frappe.ui.Slide = class Slide {
    - ${__("Add More")} + + +
    `).appendTo(this.$wrapper); @@ -35,9 +37,9 @@ frappe.ui.Slide = class Slide { this.$form = this.$body.find(".form"); this.$primary_btn = this.slides_footer.find('.primary'); - if(this.help) this.$content.append($(`

    ${this.help}

    `)); if(this.image_src) this.$content.append( $(``)); + if(this.help) this.$content.append($(`

    ${this.help}

    `)); this.reqd_fields = []; @@ -69,6 +71,8 @@ frappe.ui.Slide = class Slide { this.set_reqd_fields(); } + setup_done_state() {} + // Form methods get_atomic_fields() { var fields = JSON.parse(JSON.stringify(this.fields)); @@ -122,6 +126,7 @@ frappe.ui.Slide = class Slide { if(!field.static) { if(field.label) field.label += ' ' + this.count; } + field.reqd = 0; return field; })); if(this.count === this.max_count) { @@ -163,7 +168,7 @@ frappe.ui.Slide = class Slide { } bind_primary_action() { - this.slides_footer.find(".primary").on('click', () => { + this.slides_footer.find(".primary").on('click.primary_action', () => { this.primary_action(); }); } diff --git a/frappe/public/js/frappe/ui/toolbar/search.js b/frappe/public/js/frappe/ui/toolbar/search.js index acc623fa20..042eefc316 100644 --- a/frappe/public/js/frappe/ui/toolbar/search.js +++ b/frappe/public/js/frappe/ui/toolbar/search.js @@ -302,10 +302,16 @@ frappe.search.SearchDialog = Class.extend({ return link; } - if(result.image) { - $result.append('
    ' + result.label + '
    '); + if (result.image) { + $result.append(` +
    + ${result.label} +
    +
    `); } else if (result.image === null) { $result.append('
    ' diff --git a/frappe/public/js/frappe/ui/toolbar/user_progress_dialog.js b/frappe/public/js/frappe/ui/toolbar/user_progress_dialog.js deleted file mode 100644 index 4da50f61f2..0000000000 --- a/frappe/public/js/frappe/ui/toolbar/user_progress_dialog.js +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -// License: GNU General Public License v3. See license.txt - -frappe.provide("frappe.setup"); -frappe.provide("frappe.ui"); - -frappe.setup.UserProgressSlide = class UserProgressSlide extends frappe.ui.Slide { - constructor(slide = null) { - super(slide); - } - - make() { - super.make(); - } - - setup_done_state() { - this.$body.find(".slide-help").hide(); - this.$body.find(".form-wrapper").hide(); - this.slides_footer.find('.next-btn').addClass('btn-primary'); - this.slides_footer.find('.done-btn').hide(); - this.$primary_btn.hide(); - this.make_done_state(); - } - - make_done_state() { - this.$done_state = $(`
    -
    -

    - -
    `).appendTo(this.$body); - - this.$done_state_title = this.$done_state.find('.title'); - this.$check = this.$done_state.find('.check'); - this.$help_links = this.$done_state.find('.help-links'); - - if(this.done_state_title) { - $("" + this.done_state_title + "").appendTo(this.$done_state_title); - this.$done_state_title.on('click', () => { - frappe.set_route(this.done_state_title_route); - }); - } - - if(this.help_links) { - this.help_links.map(link => { - let $link = $(`${link.label}`); - if(link.url) { - $link.attr({"href": link.url}); - } else if(link.video_id) { - $link.on('click', () => { - frappe.help.show_video(link.video_id, link.label); - }) - } - this.$help_links.append($link); - }); - } - - } - - before_show() { - if(this.done) { - this.slides_footer.find('.next-btn').addClass('btn-primary'); - this.slides_footer.find('.done-btn').hide(); - } else { - this.slides_footer.find('.next-btn').removeClass('btn-primary'); - this.slides_footer.find('.done-btn').show(); - } - if(this.dialog_dismissed) { - this.slides_footer.find('.next-btn').removeClass('btn-primary'); - } - } - - primary_action() { - var me = this; - if(this.set_values()) { - this.slides_footer.find('.make-btn').addClass('disabled'); - frappe.call({ - method: me.submit_method, - args: {args_data: me.values}, - callback: function() { - me.done = 1; - me.refresh(); - }, - onerror: function() { - me.slides_footer.find('.make-btn').removeClass('disabled'); - }, - freeze: true - }); - } - } -}; - -frappe.setup.UserProgressDialog = class UserProgressDialog { - constructor({ - slides = [] - }) { - this.slides = slides; - this.progress_state_dict = {}; - this.slides.map(slide => { - this.progress_state_dict[slide.action_name] = slide.done || 0; - }); - this.progress_percent = 0; - this.setup(); - } - - setup() { - this.dialog = new frappe.ui.Dialog({title: __("Complete Setup")}); - this.$wrapper = $(this.dialog.$wrapper).addClass('user-progress-dialog'); - this.slide_container = new frappe.ui.Slides({ - parent: this.dialog.body, - slides: this.slides, - slide_class: frappe.setup.UserProgressSlide, - done_state: 1, - before_load: ($footer) => { - $footer.find('.text-right') - .prepend($(` - ${__("Mark as Done")}`)) - .append($(` - ${__('Create')}`)); - }, - on_update: (completed, total) => { - let percent = completed * 100 / total; - $('.user-progress .progress-bar').css({'width': percent + '%'}); - if(percent === 100) { - this.dismiss_progress(); - } - } - }); - - this.$wrapper.find('.done-btn').on('click', () => { - this.mark_as_done(); - }); - - this.get_and_update_progress_state(); - this.check_for_updates(); - } - - mark_as_done() { - let me = this; - let current_slide = this.slide_container.current_slide; - frappe.call({ - method: current_slide.mark_as_done_method, - args: {action_name: current_slide.action_name}, - callback: function() { - current_slide.done = 1; - current_slide.refresh(); - }, - freeze: true - }); - } - - check_for_updates() { - this.updater = setInterval(() => { - this.get_and_update_progress_state(); - }, 60000); - } - - get_and_update_progress_state() { - var me = this; - frappe.call({ - method: "frappe.desk.user_progress.update_and_get_user_progress", - callback: function(r) { - let states = r.message; - let changed = 0; - let completed = 0; - Object.keys(states).map(action_name => { - if(states[action_name]) { - completed ++; - } - if(me.progress_state_dict[action_name] != states[action_name]) { - changed = 1; - me.progress_state_dict[action_name] = states[action_name]; - } - }); - - if(changed) { - Object.keys(me.slide_container.slide_dict).map((id) => { - let slide = me.slide_container.slide_dict[id]; - if(me.progress_state_dict[slide.action_name]) { - if(!slide.done) { - slide.done = 1; - slide.refresh(); - } - } - }); - - } - me.progress_percent = completed / Object.keys(states).length * 100; - me.update_progress(); - }, - freeze: false - }); - } - - update_progress() { - $('.user-progress .progress-bar').css({'width': this.progress_percent + '%'}); - if(this.progress_percent === 100) { - this.dismiss_progress(); - } - } - - dismiss_progress() { - $('.user-progress').addClass('hide'); - clearInterval(this.updater); - } - - show() { - this.dialog.show(); - } -}; diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 00b6f95f06..d4caa45a89 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -264,11 +264,11 @@ Object.assign(frappe.utils, { if(has_words(["Pending", "Review", "Medium", "Not Approved"], text)) { style = "warning"; colour = "orange"; - } else if(has_words(["Open", "Urgent", "High"], text)) { + } else if(has_words(["Open", "Urgent", "High", "Failed"], text)) { style = "danger"; colour = "red"; - } else if(has_words(["Closed", "Finished", "Converted", "Completed", "Confirmed", - "Approved", "Yes", "Active", "Available", "Paid"], text)) { + } else if(has_words(["Closed", "Finished", "Converted", "Completed", "Complete", "Confirmed", + "Approved", "Yes", "Active", "Available", "Paid", "Success"], text)) { style = "success"; colour = "green"; } else if(has_words(["Submitted"], text)) { diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 560cb3d17b..a7505ff8f3 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -293,6 +293,12 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { this.last_ajax.abort(); } + const query_params = this.get_query_params(); + + if (query_params.prepared_report_name) { + filters.prepared_report_name = query_params.prepared_report_name; + } + return new Promise(resolve => { this.last_ajax = frappe.call({ method: 'frappe.desk.query_report.run', @@ -303,7 +309,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { }, callback: resolve, always: () => this.page.btn_secondary.prop('disabled', false) - }) + }); }).then(r => { let data = r.message; this.hide_status(); @@ -313,18 +319,18 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { if (data.prepared_report) { this.prepared_report = true; - const query_string = frappe.utils.get_query_string(frappe.get_route_str()); - const query_params = frappe.utils.get_query_params(query_string); // If query_string contains prepared_report_name then set filters // to match the mentioned prepared report doc and disable editing - if(query_params.prepared_report_name) { + if (query_params.prepared_report_name) { this.prepared_report_action = "Edit"; const filters_from_report = JSON.parse(data.doc.filters); Object.values(this.filters).forEach(function(field) { if (filters_from_report[field.fieldname]) { field.set_input(filters_from_report[field.fieldname]); } - field.input.disabled = true; + if (field.input) { + field.input.disabled = true; + } }); } this.add_prepared_report_buttons(data.doc); @@ -357,6 +363,12 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { }); } + get_query_params() { + const query_string = frappe.utils.get_query_string(frappe.get_route_str()); + const query_params = frappe.utils.get_query_params(query_string); + return query_params; + } + add_prepared_report_buttons(doc) { if(doc){ this.page.add_inner_button(__("Download Report"), function (){ diff --git a/frappe/public/less/desk.less b/frappe/public/less/desk.less index 5a515b6e84..d8f5a141f4 100644 --- a/frappe/public/less/desk.less +++ b/frappe/public/less/desk.less @@ -959,28 +959,33 @@ input[type="checkbox"] { } } -// User Progress Dialog -.user-progress-dialog { - .slides-progress { - margin-top: 15px; +// Onboarding Dialog +.onboarding-dialog { + + .modal-dialog { + width: 50%; + height: 80%; + max-width: none; } - .done-state { - .check-container { - font-size: 64px; - margin: 40px; - } + .onboarding-icon { + color: #3246F5; + margin-right: 5px; + } - .title { - font-weight: normal; - } + .modal-content .slide-container { + height: auto; + min-height: 100%; + bottom: 0; + } - .help-links { + img { + max-width: 128px; + max-height: 128px; + } - a { - margin: 0px 10px; - } - } + .slides-progress { + margin-top: 15px; } } diff --git a/frappe/public/less/list.less b/frappe/public/less/list.less index efcbad4653..bd6ea7e9d0 100644 --- a/frappe/public/less/list.less +++ b/frappe/public/less/list.less @@ -4,6 +4,22 @@ .result, .no-result, .freeze { min-height: ~"calc(100vh - 284px)"; } + + .msg-box { + margin-bottom: 8em; + // To compensate for percieved centering + + .null-state { + height: 12em !important; + width: auto; + } + + .meta-description { + width: 45%; + margin-right: auto; + margin-left: auto; + } + } } .freeze-row { diff --git a/frappe/public/less/notifications.less b/frappe/public/less/notifications.less index 7a49709d16..52e1c07f1e 100644 --- a/frappe/public/less/notifications.less +++ b/frappe/public/less/notifications.less @@ -33,7 +33,7 @@ } .category-list[data-category="Open Documents"] li a:hover { - background-color: #f0f4f7; + background-color: #f0f4f7; } .open-doc-count { @@ -70,7 +70,7 @@ } a.recent-item:hover { - background-color: #f0f4f7; + background-color: #f0f4f7; } a.unread:hover .mark-read { @@ -102,17 +102,19 @@ a.unread:hover .mark-read { background: @light-yellow; } -.notification-timestamp { - margin-top: 5px; - font-size: 11px; - display: inline-block; - margin-bottom: 10px; -} +.recent-item { + .notification-timestamp { + margin-top: 5px; + font-size: 11px; + display: inline-block; + margin-bottom: 10px; + } -.user-avatar { - float: left; - margin-right: 10px; - margin-bottom: 30px; + .user-avatar { + float: left; + margin-right: 10px; + margin-bottom: 30px; + } } .event-time { diff --git a/frappe/public/less/sidebar.less b/frappe/public/less/sidebar.less index 985a2b1edb..61d47712f3 100644 --- a/frappe/public/less/sidebar.less +++ b/frappe/public/less/sidebar.less @@ -450,7 +450,7 @@ body[data-route^="Module"] .main-menu { flex-basis: 50%; text-align: center; align-self: center; - font-size: @text-small; + font-size: 9px; } cursor: pointer; } diff --git a/frappe/sessions.py b/frappe/sessions.py index ffad4b5e7b..cca40cbc55 100644 --- a/frappe/sessions.py +++ b/frappe/sessions.py @@ -155,7 +155,7 @@ def get(): bootinfo["disable_async"] = frappe.conf.disable_async bootinfo["setup_complete"] = cint(frappe.db.get_single_value('System Settings', 'setup_complete')) - + bootinfo["is_first_startup"] = cint(frappe.db.get_single_value('System Settings', 'is_first_startup')) return bootinfo diff --git a/frappe/templates/print_formats/standard.html b/frappe/templates/print_formats/standard.html index 5e707cddcb..77b8f09bb7 100644 --- a/frappe/templates/print_formats/standard.html +++ b/frappe/templates/print_formats/standard.html @@ -21,7 +21,7 @@ {% endif %} {% for section in page %} -
    +
    {%- if doc.print_line_breaks and loop.index != 1 -%}
    {%- endif -%} {%- if doc.print_section_headings and section.label and section.has_data -%}

    {{ _(section.label) }}

    diff --git a/frappe/test_runner.py b/frappe/test_runner.py index 76140e442c..0be19c6110 100644 --- a/frappe/test_runner.py +++ b/frappe/test_runner.py @@ -36,6 +36,9 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(), with open(frappe.get_app_path(app, doctype_list_path), 'r') as f: doctype = f.read().strip().splitlines() + if ui_tests: + print("Selenium testing has been deprecated\nUse bench --site {site_name} run-ui-tests for Cypress tests") + xmloutput_fh = None if junit_xml_output: xmloutput_fh = open(junit_xml_output, 'w') @@ -71,7 +74,7 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(), else: ret = run_all_tests(app, verbose, profile, ui_tests, failfast=failfast) - frappe.db.commit() + if frappe.db: frappe.db.commit() # workaround! since there is no separate test db frappe.clear_cache() @@ -170,21 +173,6 @@ def run_tests_for_module(module, verbose=False, tests=(), profile=False): return _run_unittest(module, verbose=verbose, tests=tests, profile=profile) -def run_setup_wizard_ui_test(app=None, verbose=False, profile=False): - '''Run setup wizard UI test using test_test_runner''' - frappe.flags.run_setup_wizard_ui_test = 1 - return run_ui_tests(app=app, test=None, verbose=verbose, profile=profile) - -def run_ui_tests(app=None, test=None, test_list=None, verbose=False, profile=False): - '''Run a single unit test for UI using test_test_runner''' - module = importlib.import_module('frappe.tests.ui.test_test_runner') - frappe.flags.ui_test_app = app - if test_list: - frappe.flags.ui_test_list = test_list - else: - frappe.flags.ui_test_path = test - return _run_unittest(module, verbose=verbose, tests=(), profile=profile) - def _run_unittest(modules, verbose=False, tests=(), profile=False): test_suite = unittest.TestSuite() diff --git a/frappe/tests/__init__.py b/frappe/tests/__init__.py index dda38c93f9..e69de29bb2 100644 --- a/frappe/tests/__init__.py +++ b/frappe/tests/__init__.py @@ -1,46 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -from __future__ import unicode_literals - -import frappe -from werkzeug.wrappers import Request -from werkzeug.test import EnvironBuilder - -def set_request(**kwargs): - builder = EnvironBuilder(**kwargs) - frappe.local.request = Request(builder.get_environ()) - -def insert_test_data(doctype, sort_fn=None): - import frappe.model - data = get_test_doclist(doctype) - if sort_fn: - data = sorted(data, key=sort_fn) - - for doclist in data: - frappe.insert(doclist) - -def get_test_doclist(doctype, name=None): - """get test doclist, collection of doclists""" - import os - from frappe import conf - from frappe.modules.utils import peval_doclist - from frappe.modules import scrub - - doctype = scrub(doctype) - doctype_path = os.path.join(os.path.dirname(os.path.abspath(conf.__file__)), - conf.test_data_path, doctype) - - if name: - with open(os.path.join(doctype_path, scrub(name) + ".json"), 'r') as txtfile: - doclist = peval_doclist(txtfile.read()) - - return doclist - - else: - all_doclists = [] - for fname in filter(lambda n: n.endswith(".json"), os.listdir(doctype_path)): - with open(os.path.join(doctype_path, scrub(fname)), 'r') as txtfile: - all_doclists.append(peval_doclist(txtfile.read())) - - return all_doclists diff --git a/frappe/tests/test_api.py b/frappe/tests/test_api.py index 232b2be4a8..19ea9a0124 100644 --- a/frappe/tests/test_api.py +++ b/frappe/tests/test_api.py @@ -5,13 +5,14 @@ from __future__ import unicode_literals import unittest, frappe, os from frappe.core.doctype.user.user import generate_keys from frappe.frappeclient import FrappeClient +from frappe.utils.data import get_url import requests import base64 class TestAPI(unittest.TestCase): def test_insert_many(self): - server = FrappeClient(frappe.get_site_config().host_name, "Administrator", "admin", verify=False) + server = FrappeClient(get_url(), "Administrator", "admin", verify=False) frappe.db.sql("delete from `tabNote` where title in ('Sing','a','song','of','sixpence')") frappe.db.commit() @@ -30,7 +31,7 @@ class TestAPI(unittest.TestCase): self.assertTrue(frappe.db.get_value('Note', {'title': 'sixpence'})) def test_create_doc(self): - server = FrappeClient(frappe.get_site_config().host_name, "Administrator", "admin", verify=False) + server = FrappeClient(get_url(), "Administrator", "admin", verify=False) frappe.db.sql("delete from `tabNote` where title = 'test_create'") frappe.db.commit() @@ -39,13 +40,13 @@ class TestAPI(unittest.TestCase): self.assertTrue(frappe.db.get_value('Note', {'title': 'test_create'})) def test_list_docs(self): - server = FrappeClient(frappe.get_site_config().host_name, "Administrator", "admin", verify=False) + server = FrappeClient(get_url(), "Administrator", "admin", verify=False) doc_list = server.get_list("Note") self.assertTrue(len(doc_list)) def test_get_doc(self): - server = FrappeClient(frappe.get_site_config().host_name, "Administrator", "admin", verify=False) + server = FrappeClient(get_url(), "Administrator", "admin", verify=False) frappe.db.sql("delete from `tabNote` where title = 'get_this'") frappe.db.commit() @@ -56,7 +57,7 @@ class TestAPI(unittest.TestCase): self.assertTrue(doc) def test_update_doc(self): - server = FrappeClient(frappe.get_site_config().host_name, "Administrator", "admin", verify=False) + server = FrappeClient(get_url(), "Administrator", "admin", verify=False) frappe.db.sql("delete from `tabNote` where title in ('Sing','sing')") frappe.db.commit() @@ -68,7 +69,7 @@ class TestAPI(unittest.TestCase): self.assertTrue(doc["title"] == changed_title) def test_delete_doc(self): - server = FrappeClient(frappe.get_site_config().host_name, "Administrator", "admin", verify=False) + server = FrappeClient(get_url(), "Administrator", "admin", verify=False) frappe.db.sql("delete from `tabNote` where title = 'delete'") frappe.db.commit() @@ -90,21 +91,21 @@ class TestAPI(unittest.TestCase): api_key = frappe.db.get_value("User", "Administrator", "api_key") header = {"Authorization": "token {}:{}".format(api_key, generated_secret)} - res = requests.post(frappe.get_site_config().host_name + "/api/method/frappe.auth.get_logged_user", headers=header) + res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header) self.assertEqual(res.status_code, 200) self.assertEqual("Administrator", res.json()["message"]) self.assertEqual(keys['api_secret'], generated_secret) header = {"Authorization": "Basic {}".format(base64.b64encode(frappe.safe_encode("{}:{}".format(api_key, generated_secret))).decode())} - res = requests.post(frappe.get_site_config().host_name + "/api/method/frappe.auth.get_logged_user", headers=header) + res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header) self.assertEqual(res.status_code, 200) self.assertEqual("Administrator", res.json()["message"]) # Valid api key, invalid api secret api_secret = "ksk&93nxoe3os" header = {"Authorization": "token {}:{}".format(api_key, api_secret)} - res = requests.post(frappe.get_site_config().host_name + "/api/method/frappe.auth.get_logged_user", headers=header) + res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header) self.assertEqual(res.status_code, 403) @@ -112,5 +113,5 @@ class TestAPI(unittest.TestCase): api_key = "@3djdk3kld" api_secret = "ksk&93nxoe3os" header = {"Authorization": "token {}:{}".format(api_key, api_secret)} - res = requests.post(frappe.get_site_config().host_name + "/api/method/frappe.auth.get_logged_user", headers=header) + res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header) self.assertEqual(res.status_code, 401) diff --git a/frappe/tests/test_frappeoauth2client.py b/frappe/tests/test_frappeoauth2client.py deleted file mode 100644 index ebf09adf6d..0000000000 --- a/frappe/tests/test_frappeoauth2client.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt -from __future__ import unicode_literals - -import unittest, frappe, requests, time -from frappe.test_runner import make_test_records -from frappe.utils.selenium_testdriver import TestDriver -from six.moves.urllib.parse import urlparse -from frappe.frappeclient import FrappeOAuth2Client - -class TestFrappeOAuth2Client(unittest.TestCase): - def setUp(self): - self.driver = TestDriver() - make_test_records("OAuth Client") - make_test_records("User") - self.client_id = frappe.get_all("OAuth Client", fields=["*"])[0].get("client_id") - - # Set Frappe server URL reqired for id_token generation - try: - frappe_login_key = frappe.get_doc("Social Login Key", "frappe") - except frappe.DoesNotExistError: - frappe_login_key = frappe.new_doc("Social Login Key") - frappe_login_key.get_social_login_provider("Frappe", initialize=True) - frappe_login_key.base_url = "http://localhost:8000" - frappe_login_key.save() - - def test_insert_note(self): - - # Go to Authorize url - self.driver.get( - "api/method/frappe.integrations.oauth2.authorize?client_id=" + - self.client_id + - "&scope=all%20openid&response_type=code&redirect_uri=http%3A%2F%2Flocalhost" - ) - - time.sleep(2) - - # Login - username = self.driver.find("#login_email")[0] - username.send_keys("test@example.com") - - password = self.driver.find("#login_password")[0] - password.send_keys("Eastern_43A1W") - - sign_in = self.driver.find(".btn-login")[0] - sign_in.submit() - - time.sleep(2) - - # Allow access to resource - allow = self.driver.find("#allow")[0] - allow.click() - - time.sleep(2) - - # Get authorization code from redirected URL - auth_code = urlparse(self.driver.driver.current_url).query.split("=")[1] - - payload = "grant_type=authorization_code&code=" - payload += auth_code - payload += "&redirect_uri=http%3A%2F%2Flocalhost&client_id=" - payload += self.client_id - - headers = {'content-type':'application/x-www-form-urlencoded'} - - # Request for bearer token - token_response = requests.post( frappe.get_site_config().host_name + - "/api/method/frappe.integrations.oauth2.get_token", data=payload, headers=headers) - - # Parse bearer token json - bearer_token = token_response.json() - client = FrappeOAuth2Client(frappe.get_site_config().host_name, bearer_token.get("access_token")) - - notes = [ - {"doctype": "Note", "title": "Sing", "public": True}, - {"doctype": "Note", "title": "a", "public": True}, - {"doctype": "Note", "title": "Song", "public": True}, - {"doctype": "Note", "title": "of", "public": True}, - {"doctype": "Note", "title": "sixpence", "public": True} - ] - - for note in notes: - client.insert(note) - - self.assertTrue(len(frappe.get_all("Note")) == 5) diff --git a/frappe/tests/test_recorder.py b/frappe/tests/test_recorder.py index bffddc4ea1..d45ef12e86 100644 --- a/frappe/tests/test_recorder.py +++ b/frappe/tests/test_recorder.py @@ -7,7 +7,7 @@ from __future__ import unicode_literals import unittest import frappe import frappe.recorder -from .test_website import set_request +from frappe.utils import set_request import sqlparse diff --git a/frappe/tests/test_scheduler.py b/frappe/tests/test_scheduler.py index 1f8c7943c4..e554fd23be 100644 --- a/frappe/tests/test_scheduler.py +++ b/frappe/tests/test_scheduler.py @@ -2,75 +2,81 @@ from __future__ import unicode_literals from unittest import TestCase from dateutil.relativedelta import relativedelta -from frappe.utils.scheduler import (enqueue_applicable_events, restrict_scheduler_events_if_dormant, - get_enabled_scheduler_events) -from frappe import _dict -from frappe.utils.background_jobs import enqueue -from frappe.utils import now_datetime, today, add_days, add_to_date +from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs +from frappe.utils.background_jobs import enqueue, get_jobs +from frappe.utils.scheduler import enqueue_events, is_dormant, schedule_jobs_based_on_activity +from frappe.utils import add_days, get_datetime import frappe import time def test_timeout(): - '''This function needs to be pickleable''' time.sleep(100) +def test_timeout_10(): + time.sleep(10) + +def test_method(): + pass + class TestScheduler(TestCase): def setUp(self): - frappe.db.set_global('enabled_scheduler_events', "") - frappe.flags.ran_schedulers = [] + if not frappe.get_all('Scheduled Job Type', limit=1): + sync_jobs() - def test_all_events(self): - last = now_datetime() - relativedelta(hours=2) - enqueue_applicable_events(frappe.local.site, now_datetime(), last) - self.assertTrue("all" in frappe.flags.ran_schedulers) + def test_enqueue_jobs(self): + frappe.db.sql("update `tabScheduled Job Type` set last_execution = '2010-01-01 00:00:00'") - def test_enabled_events(self): - frappe.flags.enabled_events = ["hourly", "hourly_long", "daily", "daily_long", - "weekly", "weekly_long", "monthly", "monthly_long"] + frappe.flags.execute_job = True + enqueue_events(site = frappe.local.site) + frappe.flags.execute_job = False - # maintain last_event and next_event on the same day - last_event = now_datetime().replace(hour=0, minute=0, second=0, microsecond=0) - next_event = last_event + relativedelta(minutes=30) + self.assertTrue('frappe.email.queue.clear_outbox', frappe.flags.enqueued_jobs) + self.assertTrue('frappe.utils.change_log.check_for_update', frappe.flags.enqueued_jobs) + self.assertTrue('frappe.email.doctype.auto_email_report.auto_email_report.send_monthly', frappe.flags.enqueued_jobs) - enqueue_applicable_events(frappe.local.site, next_event, last_event) - self.assertFalse("cron" in frappe.flags.ran_schedulers) + def test_queue_peeking(self): + job = get_test_job() - # maintain last_event and next_event on the same day - last_event = now_datetime().replace(hour=0, minute=0, second=0, microsecond=0) - next_event = last_event + relativedelta(hours=2) + self.assertTrue(job.enqueue()) + job.db_set('last_execution', '2010-01-01 00:00:00') + frappe.db.commit() - frappe.flags.ran_schedulers = [] - enqueue_applicable_events(frappe.local.site, next_event, last_event) - self.assertTrue("all" in frappe.flags.ran_schedulers) - self.assertTrue("hourly" in frappe.flags.ran_schedulers) + # 1 job in queue + self.assertTrue(job.enqueue()) + job.db_set('last_execution', '2010-01-01 00:00:00') + frappe.db.commit() - frappe.flags.enabled_events = None + # 2nd job not loaded + self.assertFalse(job.enqueue()) + job.delete() - def test_enabled_events_day_change(self): + def test_is_dormant(self): + self.assertTrue(is_dormant(check_time= get_datetime('2100-01-01 00:00:00'))) + self.assertTrue(is_dormant(check_time = add_days(frappe.db.get_last_created('Activity Log'), 5))) + self.assertFalse(is_dormant(check_time = frappe.db.get_last_created('Activity Log'))) - # use flags instead of globals as this test fails intermittently - # the root cause has not been identified but the culprit seems cache - # since cache is mutable, it maybe be changed by a parallel process - frappe.flags.enabled_events = ["daily", "daily_long", "weekly", "weekly_long", - "monthly", "monthly_long"] - - # maintain last_event and next_event on different days - next_event = now_datetime().replace(hour=0, minute=0, second=0, microsecond=0) - last_event = next_event - relativedelta(hours=2) - - frappe.flags.ran_schedulers = [] - enqueue_applicable_events(frappe.local.site, next_event, last_event) - self.assertTrue("all" in frappe.flags.ran_schedulers) - self.assertFalse("hourly" in frappe.flags.ran_schedulers) - - frappe.flags.enabled_events = None + def test_once_a_day_for_dormant(self): + frappe.db.clear_table('Scheduled Job Log') + self.assertTrue(schedule_jobs_based_on_activity(check_time= get_datetime('2100-01-01 00:00:00'))) + self.assertTrue(schedule_jobs_based_on_activity(check_time = add_days(frappe.db.get_last_created('Activity Log'), 5))) + # create a fake job executed 5 days from now + job = get_test_job(method='frappe.tests.test_scheduler.test_method', frequency='Daily') + job.execute() + job_log = frappe.get_doc('Scheduled Job Log', dict(scheduled_job_type=job.name)) + job_log.db_set('creation', add_days(frappe.db.get_last_created('Activity Log'), 5)) + # inactive site with recent job, don't run + self.assertFalse(schedule_jobs_based_on_activity(check_time = add_days(frappe.db.get_last_created('Activity Log'), 5))) + # one more day has passed + self.assertTrue(schedule_jobs_based_on_activity(check_time = add_days(frappe.db.get_last_created('Activity Log'), 6))) + frappe.db.rollback() def test_job_timeout(self): + return job = enqueue(test_timeout, timeout=10) count = 5 while count > 0: @@ -81,5 +87,19 @@ class TestScheduler(TestCase): self.assertTrue(job.is_failed) - def tearDown(self): - frappe.flags.ran_schedulers = [] +def get_test_job(method='frappe.tests.test_scheduler.test_timeout_10', frequency='All'): + if not frappe.db.exists('Scheduled Job Type', dict(method=method)): + job = frappe.get_doc(dict( + doctype = 'Scheduled Job Type', + method = method, + last_execution = '2010-01-01 00:00:00', + frequency = frequency + )).insert() + else: + job = frappe.get_doc('Scheduled Job Type', dict(method=method)) + job.db_set('last_execution', '2010-01-01 00:00:00') + job.db_set('frequency', frequency) + frappe.db.commit() + + return job + diff --git a/frappe/tests/test_sitemap.py b/frappe/tests/test_sitemap.py index 6f98e3ed48..455a80eb3e 100644 --- a/frappe/tests/test_sitemap.py +++ b/frappe/tests/test_sitemap.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals import frappe, unittest -from frappe.tests.test_website import get_html_for_route +from frappe.utils import get_html_for_route class TestSitemap(unittest.TestCase): def test_sitemap(self): diff --git a/frappe/tests/test_twofactor.py b/frappe/tests/test_twofactor.py index 27129d9832..d18a25b43e 100644 --- a/frappe/tests/test_twofactor.py +++ b/frappe/tests/test_twofactor.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import unittest, frappe, pyotp from frappe.auth import HTTPRequest from frappe.utils import cint -from frappe.tests import set_request +from frappe.utils import set_request from frappe.auth import validate_ip_address from frappe.twofactor import (should_run_2fa, authenticate_for_2factor, get_cached_user_pass, two_factor_is_enabled_for_, confirm_otp_token, get_otpsecret_for_, get_verification_obj) @@ -157,8 +157,8 @@ def create_http_request(): '''Get http request object.''' set_request(method='POST', path='login') enable_2fa() - frappe.form_dict['usr'] = 'test@erpnext.com' - frappe.form_dict['pwd'] = 'test' + frappe.form_dict['usr'] = 'test@example.com' + frappe.form_dict['pwd'] = 'Eastern_43A1W' frappe.local.form_dict['cmd'] = 'login' http_requests = HTTPRequest() return http_requests diff --git a/frappe/tests/test_website.py b/frappe/tests/test_website.py index 9efd4d04a1..d114f7d1b2 100644 --- a/frappe/tests/test_website.py +++ b/frappe/tests/test_website.py @@ -1,23 +1,13 @@ from __future__ import unicode_literals -import frappe, unittest -from werkzeug.wrappers import Request -from werkzeug.test import EnvironBuilder +import unittest +import frappe from frappe.website import render +from frappe.utils import set_request -def set_request(**kwargs): - builder = EnvironBuilder(**kwargs) - frappe.local.request = Request(builder.get_environ()) - -def get_html_for_route(route): - set_request(method='GET', path=route) - response = render.render() - html = frappe.safe_decode(response.get_data()) - return html class TestWebsite(unittest.TestCase): - def test_page_load(self): frappe.set_user('Guest') set_request(method='POST', path='login') @@ -76,4 +66,3 @@ class TestWebsite(unittest.TestCase): delattr(frappe.hooks, 'website_redirects') frappe.cache().delete_key('app_hooks') - diff --git a/frappe/tests/ui/data/test_lib.js b/frappe/tests/ui/data/test_lib.js deleted file mode 100644 index 0fbbc2b748..0000000000 --- a/frappe/tests/ui/data/test_lib.js +++ /dev/null @@ -1,249 +0,0 @@ -frappe.tests = { - data: {}, - make: function(doctype, data) { - let dialog_is_active = () => { - return ( - cur_dialog && (!cur_frm || cur_frm.doc.doctype != doctype) - ); - }; - return frappe.run_serially([ - () => frappe.set_route('List', doctype), - () => frappe.new_doc(doctype), - () => { - if (frappe.quick_entry) { - frappe.quick_entry.dialog.$wrapper.find('.edit-full').click(); - return frappe.timeout(1); - } else { - let root_node; - if (cur_tree) { - for (const key in cur_tree.nodes) { - if (cur_tree.nodes[key].parent_label && cur_tree.nodes[key].expandable) { - root_node = cur_tree.nodes[key].label; - break; - } - } - } - if (root_node){ - frappe.tests.open_add_child_dialog(root_node); - return frappe.timeout(1); - } - } - }, - () => { - if(dialog_is_active()) { - return frappe.tests.set_dialog_values(cur_dialog, data); - } else { - return frappe.tests.set_form_values(cur_frm, data); - } - }, - - () => { - if(dialog_is_active()) { - return cur_dialog.get_primary_btn().click(); - } else { - return frappe.quick_entry ? frappe.quick_entry.insert() : cur_frm.save(); - } - } - ]); - }, - open_add_child_dialog: (root_node) => { - frappe.tests.click_link(root_node); - frappe.timeout(1); - frappe.tests.click_button('Add Child'); - }, - set_form_values: (frm, data) => { - let tasks = []; - - data.forEach(item => { - for (let key in item) { - let task = () => { - let value = item[key]; - if ($.isArray(value)) { - return frappe.tests.set_grid_values(frm, key, value); - } else { - // single value - return frm.set_value(key, value); - } - }; - tasks.push(task); - tasks.push(frappe.after_ajax); - tasks.push(() => frappe.timeout(0.4)); - } - }); - - // set values - return frappe.run_serially(tasks); - - }, - set_dialog_values: (dialog, data) => { - let tasks = []; - - data.forEach(item => { - for (let key in item) { - let task = () => { - let value = item[key]; - return dialog.set_value(key, value); - }; - tasks.push(task); - tasks.push(frappe.after_ajax); - tasks.push(() => frappe.timeout(0.4)); - } - }); - - return frappe.run_serially(tasks); - }, - set_grid_values: (frm, key, value) => { - // set value in grid - let grid = frm.get_field(key).grid; - grid.remove_all(); - - let grid_row_tasks = []; - - // build tasks for each row - value.forEach(d => { - grid_row_tasks.push(() => { - - let grid_value_tasks = []; - grid_value_tasks.push(() => grid.add_new_row()); - grid_value_tasks.push(() => grid.get_row(-1).toggle_view(true)); - grid_value_tasks.push(() => frappe.timeout(0.5)); - - // build tasks to set each row value - d.forEach(child_value => { - for (let child_key in child_value) { - grid_value_tasks.push(() => { - let grid_row = grid.get_row(-1); - return frappe.model.set_value(grid_row.doc.doctype, - grid_row.doc.name, child_key, child_value[child_key]); - }); - grid_value_tasks.push(frappe.after_ajax); - grid_value_tasks.push(() => frappe.timeout(0.4)); - } - }); - - return frappe.run_serially(grid_value_tasks); - }); - }); - return frappe.run_serially(grid_row_tasks); - }, - setup_doctype: (doctype, data) => { - return frappe.run_serially([ - () => frappe.set_route('List', doctype), - () => frappe.timeout(1), - () => { - frappe.tests.data[doctype] = []; - let expected = Object.keys(data); - cur_list.data.forEach((d) => { - frappe.tests.data[doctype].push(d.name); - if(expected.indexOf(d.name) !== -1) { - expected[expected.indexOf(d.name)] = null; - } - }); - - let tasks = []; - - expected.forEach(function(d) { - if(d) { - tasks.push(() => frappe.tests.make(doctype, - data[d])); - } - }); - - return frappe.run_serially(tasks); - }]); - }, - click_page_head_item: (text) => { - // Method to items present on the page header like New, Save, Delete etc. - let possible_texts = ["New", "Delete", "Save", "Yes"]; - return frappe.run_serially([ - () => { - if (text == "Menu"){ - $(`span.menu-btn-group-label:contains('Menu'):visible`).click(); - } else if (text == "Refresh") { - $(`.btn-secondary:contains('Refresh'):visible`).click(); - } else if (possible_texts.includes(text)) { - $(`.btn-primary:contains("${text}"):visible`).click(); - } - }, - () => frappe.timeout(1) - ]); - }, - click_dropdown_item: (text) => { - // Method to click dropdown elements - return frappe.run_serially([ - () => { - let li = $(`.dropdown-menu li:contains("${text}"):visible`).get(0); - $(li).find(`a`).click(); - }, - () => frappe.timeout(1) - ]); - }, - click_desktop_icon: (text) => { - // Method to click the desktop icons on the Desk, by their name - return frappe.run_serially([ - () => $("#icon-grid > div > div.app-icon[title="+text+"]").click(), - () => frappe.timeout(1) - ]); - }, - is_visible: (text, tag='a') => { - // Method to check the visibility of an element - return $(`${tag}:contains("${text}")`).is(`:visible`); - }, - /** - * Clicks a button on a form. - * @param {String} text - The button's text - * @return {frappe.timeout} - * @throws will throw an exception if a matching visible button is not found - */ - click_button: function(text) { - let element = $(`.btn:contains("${text}"):visible`); - if(!element.length) { - throw `did not find any button containing ${text}`; - } - element.click(); - return frappe.timeout(0.5); - }, - /** - * Clicks a link on a form. - * @param {String} text - The text of the link to be clicked - * @return {frappe.timeout} - * @throws will throw an exception if a link with the given text is not found - */ - click_link: function(text) { - let element = $(`a:contains("${text}"):visible`); - if(!element.length) { - throw `did not find any link containing ${text}`; - } - element.get(0).click(); - return frappe.timeout(0.5); - }, - /** - * Sets the given control to the value given. - * @param {String} fieldname - The Doctype's field name - * @param {String} value - The value the control should be changed to - * @return {frappe.timeout} - * @throws will throw an exception if the field is not found or is not visible - */ - set_control: function(fieldname, value) { - let control = $(`.form-control[data-fieldname="${fieldname}"]:visible`); - if(!control.length) { - throw `did not find any control with fieldname ${fieldname}`; - } - control.val(value).trigger('change'); - return frappe.timeout(0.5); - }, - /** - * Checks if given field is disabled. - * @param {String} fieldname - The Doctype field name - * @return {Boolean} true if condition is met - * @throws will throw an exception if the field is not found or is not a form control - */ - is_disabled_field: function(fieldname){ - let control = $(`.form-control[data-fieldname="${fieldname}"]:disabled`); - if(!control.length) { - throw `did not find any control with fieldname ${fieldname}`; - } else { - return true; - } - } -}; \ No newline at end of file diff --git a/frappe/tests/ui/test_calendar_view.js b/frappe/tests/ui/test_calendar_view.js deleted file mode 100644 index 914f6174da..0000000000 --- a/frappe/tests/ui/test_calendar_view.js +++ /dev/null @@ -1,79 +0,0 @@ -QUnit.module('views'); - -QUnit.test("Calendar View Tests", function(assert) { - assert.expect(3); - let done = assert.async(); - let random_text = frappe.utils.get_random(3); - let today = frappe.datetime.get_today()+" 16:20:35"; //arbitrary value taken to prevent cases like 12a for 12:00am and 12h to 24h conversion - let visible_time = () => { - // Method to return the start-time (hours) of the event visible - return $('.fc-time').text().split('p')[0]; // 'p' because the arbitrary time is pm - }; - let event_title_text = () => { - // Method to return the title of the event visible - return $('.fc-title:visible').text(); - }; - - frappe.run_serially([ - // create 2 events, one private, one public - () => frappe.tests.make("Event", [ - {subject: random_text + ':Pri'}, - {starts_on: today}, - {event_type: 'Private'} - ]), - - () => frappe.timeout(1), - - () => frappe.tests.make("Event", [ - {subject: random_text + ':Pub'}, - {starts_on: today}, - {event_type: 'Public'} - ]), - - () => frappe.timeout(1), - - // Goto Calendar view - () => frappe.set_route(["List", "Event", "Calendar"]), - - // clear filter - () => cur_list.filter_area.remove('event_type'), - () => frappe.timeout(2), - // Check if event is created - () => { - // Check if the event exists and if its title matches with the one created - assert.ok(event_title_text().includes(random_text + ':Pri'), - "Event title verified"); - }, - - // check filter - () => cur_list.filter_area.add('Event', 'event_type', '=', 'Public'), - () => frappe.timeout(1), - () => { - // private event should be hidden - assert.notOk(event_title_text().includes(random_text + ':Pri'), - "Event title verified"); - }, - - // Delete event - // Goto Calendar view - () => frappe.set_route(["List", "Event", "Calendar"]), - () => frappe.timeout(1), - // delete event - () => frappe.click_link(random_text + ':Pub'), - () => { - frappe.tests.click_page_head_item('Menu'); - frappe.tests.click_dropdown_item('Delete'); - }, - () => frappe.timeout(0.5), - () => frappe.click_button('Yes'), - () => frappe.timeout(2), - () => frappe.set_route(["List", "Event", "Calendar"]), - () => frappe.click_button("Refresh"), - () => frappe.timeout(1), - - // Check if event is deleted - () => assert.notOk(event_title_text().includes(random_text + ':Pub'), - "Event deleted"), - () => done() - ]); -}); \ No newline at end of file diff --git a/frappe/tests/ui/test_control_geolocation.js b/frappe/tests/ui/test_control_geolocation.js deleted file mode 100644 index 0e3bedda26..0000000000 --- a/frappe/tests/ui/test_control_geolocation.js +++ /dev/null @@ -1,39 +0,0 @@ -QUnit.module('controls'); - -QUnit.test("Test ControlGeolocation", function(assert) { - assert.expect(1); - - const random_name = frappe.utils.get_random(3).toLowerCase(); - - let done = assert.async(); - - // geolocation alert dialog suppressed (only secure origins or localhost allowed) - window.alert = function() { - console.log.apply(console, arguments); //eslint-disable-line - }; - - frappe.run_serially([ - () => { - return frappe.tests.make('Custom Field', [ - {dt: 'ToDo'}, - {fieldtype: 'Geolocation'}, - {label: random_name}, - ]); - }, - () => frappe.set_route('List', 'ToDo'), - () => frappe.new_doc('ToDo'), - () => { - if (frappe.quick_entry) - { - frappe.quick_entry.dialog.$wrapper.find('.edit-full').click(); - return frappe.timeout(1); - } - }, - () => { - const control = $(`.frappe-control[data-fieldname="${random_name}"]`); - - return assert.ok(control.data('fieldtype') === 'Geolocation'); - }, - () => done() - ]); -}); diff --git a/frappe/tests/ui/test_control_html.js b/frappe/tests/ui/test_control_html.js deleted file mode 100644 index 0cb70cc4fd..0000000000 --- a/frappe/tests/ui/test_control_html.js +++ /dev/null @@ -1,57 +0,0 @@ -QUnit.module('controls'); - -QUnit.test("Test ControlHTML", function(assert) { - assert.expect(3); - const random_name = frappe.utils.get_random(3).toLowerCase(); - - let done = assert.async(); - - frappe.run_serially([ - () => { - return frappe.tests.make('Custom Field', [ - {dt: 'ToDo'}, - {fieldtype: 'HTML'}, - {label: random_name}, - {options: '

    Test

    '} - ]); - }, - () => { - return frappe.tests.make('Custom Field', [ - {dt: 'ToDo'}, - {fieldtype: 'HTML'}, - {label: random_name + "_template"}, - {options: '

    Test {{ doc.status }}

    '} - ]); - }, - () => frappe.set_route('List', 'ToDo'), - () => frappe.new_doc('ToDo'), - () => { - if (frappe.quick_entry) - { - frappe.quick_entry.dialog.$wrapper.find('.edit-full').click(); - return frappe.timeout(1); - } - }, - () => { - const control = $(`.frappe-control[data-fieldname="${random_name}"]`)[0]; - - return assert.ok(control.innerHTML === '

    Test

    '); - }, - () => { - const control = $(`.frappe-control[data-fieldname="${random_name}_template"]`)[0]; - // refresh input must be called independently - cur_frm.get_field(`${random_name}_template`).refresh_input(); - - return assert.ok(control.innerHTML === '

    Test Open

    '); - }, - () => frappe.tests.set_control("status", "Closed"), - () => frappe.timeout(1), - () => { - const control = $(`.frappe-control[data-fieldname="${random_name}_template"]`)[0]; - // refresh input must be called independently - cur_frm.get_field(`${random_name}_template`).refresh_input(); - return assert.ok(control.innerHTML === '

    Test Closed

    '); - }, - () => done() - ]); -}); diff --git a/frappe/tests/ui/test_kanban/test_kanban_column.js b/frappe/tests/ui/test_kanban/test_kanban_column.js deleted file mode 100644 index 0c4593a8dd..0000000000 --- a/frappe/tests/ui/test_kanban/test_kanban_column.js +++ /dev/null @@ -1,31 +0,0 @@ -QUnit.module('views'); - -QUnit.test("Test: Setting column colour [Kanban view]", function(assert) { - assert.expect(3); - let done = assert.async(); - function get_column(name, colour) { - return ('.kanban-column:contains('+name+')>div>div>ul>li>div.'+colour); - } - - frappe.run_serially([ - () => frappe.set_route("List", "ToDo", "Kanban", "Kanban test"), - () => frappe.timeout(1), - () => assert.deepEqual(["List", "ToDo", "Kanban", "Kanban test"], frappe.get_route(), - "Kanban view opened successfully."), - () => { - // set colour for columns - $(get_column('High', "red")).click(); - $(get_column('Medium', "green")).click(); - $(get_column('Low', "yellow")).click(); - }, - () => frappe.timeout(1), - () => { - //check if different colours are set - assert.equal($('.red > span')[0].innerText, 'High', - "Colour is set for kanban column."); - assert.equal($('.green > span')[0].innerText, 'Medium', - "Different colour is set for other column."); - }, - () => done() - ]); -}); \ No newline at end of file diff --git a/frappe/tests/ui/test_kanban/test_kanban_creation.js b/frappe/tests/ui/test_kanban/test_kanban_creation.js deleted file mode 100644 index 3e1afbefdf..0000000000 --- a/frappe/tests/ui/test_kanban/test_kanban_creation.js +++ /dev/null @@ -1,34 +0,0 @@ -QUnit.module('views'); - -QUnit.test("Test: Creation [Kanban view]", function(assert) { - assert.expect(2); - let done = assert.async(); - - const board_name = 'Kanban test'; - - frappe.run_serially([ - () => frappe.set_route("List", "ToDo", "List"), - // wait for cur_list to initialize - () => cur_list.init(), - // click kanban in side bar - () => frappe.tests.click_link('Kanban'), - () => frappe.tests.click_link('New Kanban Board'), - () => frappe.timeout(0.5), - // create new kanban - () => { - assert.equal(cur_dialog.title, 'New Kanban Board', - "Dialog for new kanban opened."); - cur_dialog.set_value('board_name', board_name); - cur_dialog.set_value('field_name', 'Priority'); - }, - () => frappe.timeout(0.5), - () => cur_dialog.get_primary_btn().click(), - () => frappe.timeout(1), - () => frappe.set_route("List", "Kanban Board", "List"), - () => frappe.timeout(0.5), - // check in kanban list if new kanban is created - () => assert.equal(cur_list.data[0].name, board_name, - "Added kanban is visible in kanban list."), - () => done() - ]); -}); \ No newline at end of file diff --git a/frappe/tests/ui/test_kanban/test_kanban_filters.js b/frappe/tests/ui/test_kanban/test_kanban_filters.js deleted file mode 100644 index 5e9af9f0fa..0000000000 --- a/frappe/tests/ui/test_kanban/test_kanban_filters.js +++ /dev/null @@ -1,27 +0,0 @@ -QUnit.module('views'); - -QUnit.test("Test: Filters [Kanban view]", function(assert) { - assert.expect(3); - let done = assert.async(); - - frappe.run_serially([ - () => frappe.set_route("List", "ToDo", "Kanban", "Kanban test"), - () => frappe.timeout(1), - () => { - assert.deepEqual(["List", "ToDo", "Kanban", "Kanban test"], frappe.get_route(), - "Kanban view opened successfully."); - }, - // set filter values - () => cur_list.filter_area.add('ToDo', 'priority', '=', 'Low'), - () => frappe.timeout(1), - () => cur_list.page.btn_secondary.click(), - () => frappe.timeout(1), - () => { - assert.equal(cur_list.data[0].priority, 'Low', - 'visible element has low priority'); - let non_low_items = cur_list.data.filter(d => d.priority != 'Low'); - assert.equal(non_low_items.length, 0, 'No item without low priority'); - }, - () => done() - ]); -}); \ No newline at end of file diff --git a/frappe/tests/ui/test_kanban/test_kanban_view.js b/frappe/tests/ui/test_kanban/test_kanban_view.js deleted file mode 100644 index 4ed9597cf0..0000000000 --- a/frappe/tests/ui/test_kanban/test_kanban_view.js +++ /dev/null @@ -1,29 +0,0 @@ -QUnit.module('views'); - -QUnit.test("Test: Kanban view", function(assert) { - assert.expect(4); - let done = assert.async(); - - frappe.run_serially([ - () => frappe.set_route("List", "ToDo", "List"), - // calculate number of element in list - () => frappe.timeout(1), - () => frappe.set_route("List", "ToDo", "Kanban", "Kanban test"), - () => frappe.timeout(2), - () => { - assert.equal('Kanban', cur_list.view_name, - "Current view is kanban."); - assert.equal("Kanban test", cur_list.page_title, - "Kanban view opened successfully."); - // check if all elements are visible in kanban view - const $high_priority_cards = - $('.kanban-column[data-column-value="High"] .kanban-card-wrapper'); - const $low_priority_cards = - $('.kanban-column[data-column-value="Low"] .kanban-card-wrapper'); - - assert.equal($high_priority_cards.length, 1); - assert.equal($low_priority_cards.length, 1); - }, - () => done() - ]); -}); \ No newline at end of file diff --git a/frappe/tests/ui/test_linked_with.js b/frappe/tests/ui/test_linked_with.js deleted file mode 100644 index aeaced2d19..0000000000 --- a/frappe/tests/ui/test_linked_with.js +++ /dev/null @@ -1,19 +0,0 @@ -QUnit.module('form'); - -QUnit.test("Test Linked With", function(assert) { - assert.expect(2); - const done = assert.async(); - - frappe.run_serially([ - () => frappe.set_route('Form', 'Module Def', 'Contacts'), - () => frappe.tests.click_page_head_item('Menu'), - () => frappe.tests.click_dropdown_item('Links'), - () => frappe.timeout(4), - () => { - assert.equal(cur_dialog.title, 'Linked With', 'Linked with dialog is opened'); - const link_tables_count = cur_dialog.$wrapper.find('.list-item-table').length; - assert.equal(link_tables_count, 2, 'Two DocTypes are linked with Contacts'); - }, - done - ]); -}); \ No newline at end of file diff --git a/frappe/tests/ui/test_list/test_list_filter.js b/frappe/tests/ui/test_list/test_list_filter.js deleted file mode 100644 index 059122f0c0..0000000000 --- a/frappe/tests/ui/test_list/test_list_filter.js +++ /dev/null @@ -1,39 +0,0 @@ -QUnit.module('views'); - -QUnit.test("Test list filters", function(assert) { - assert.expect(3); - let done = assert.async(); - - frappe.run_serially([ - () => { - return frappe.tests.make('ToDo', [ - {description: 'low priority'}, - {priority: 'Low'} - ]); - }, - () => { - return frappe.tests.make('ToDo', [ - {description: 'high priority'}, - {priority: 'High'} - ]); - }, - () => frappe.set_route('List', 'ToDo', 'List'), - () => frappe.timeout(0.5), - () => { - assert.deepEqual(['List', 'ToDo', 'List'], frappe.get_route(), - "List opened successfully."); - //set filter values - return frappe.set_control('priority', 'Low'); - }, - () => frappe.timeout(0.5), - () => cur_list.page.btn_secondary.click(), - () => frappe.timeout(1), - () => { - assert.equal(cur_list.data[0].priority, 'Low', - 'visible element has low priority'); - let non_low_items = cur_list.data.filter(d => d.priority != 'Low'); - assert.equal(non_low_items.length, 0, 'no item without low priority'); - }, - () => done() - ]); -}); \ No newline at end of file diff --git a/frappe/tests/ui/test_list/test_list_paging.js b/frappe/tests/ui/test_list/test_list_paging.js deleted file mode 100644 index b760a11370..0000000000 --- a/frappe/tests/ui/test_list/test_list_paging.js +++ /dev/null @@ -1,25 +0,0 @@ -QUnit.module('views'); - -QUnit.test("Test paging in list view", function(assert) { - assert.expect(5); - let done = assert.async(); - - frappe.run_serially([ - () => frappe.set_route('List', 'DocType'), - () => frappe.timeout(0.5), - () => assert.deepEqual(['List', 'DocType', 'List'], frappe.get_route(), - "List opened successfully."), - //check elements less then page length [20 in this case] - () => assert.equal(cur_list.data.length, 20, 'show 20 items'), - () => frappe.click_button('More'), - () => frappe.timeout(2), - () => assert.equal(cur_list.data.length, 40, 'show more items'), - () => frappe.tests.click_button('100'), - () => frappe.timeout(2), - () => assert.ok(cur_list.data.length > 40, 'show 100 items'), - () => frappe.tests.click_button('20'), - () => frappe.timeout(2), - () => assert.equal(cur_list.data.length, 20, 'show 20 items again'), - () => done() - ]); -}); \ No newline at end of file diff --git a/frappe/tests/ui/test_list_count.js b/frappe/tests/ui/test_list_count.js deleted file mode 100644 index 31f73964d2..0000000000 --- a/frappe/tests/ui/test_list_count.js +++ /dev/null @@ -1,33 +0,0 @@ -QUnit.module('Setup'); - -QUnit.test("Test List Count", function(assert) { - assert.expect(3); - const done = assert.async(); - - frappe.run_serially([ - () => frappe.set_route('List', 'DocType'), - () => frappe.timeout(0.5), - () => { - let count = $('.list-count').text().split(' ')[0]; - assert.equal(cur_list.data.length, count, "Correct Count"); - }, - - () => frappe.timeout(1), - () => cur_list.filter_area.add('Doctype', 'module', '=', 'Desk'), - () => frappe.click_button('Refresh'), - () => { - let count = $('.list-count').text().split(' ')[0]; - assert.equal(cur_list.data.length, count, "Correct Count"); - }, - - () => cur_list.filter_area.clear(), - () => frappe.timeout(1), - () => { - cur_list.filter_area.add('DocField', 'fieldname', 'like', 'owner'); - let count = $('.list-count').text().split(' ')[0]; - assert.equal(cur_list.data.length, count, "Correct Count"); - }, - - done - ]); -}); \ No newline at end of file diff --git a/frappe/tests/ui/test_module_view.js b/frappe/tests/ui/test_module_view.js deleted file mode 100644 index 7340945b29..0000000000 --- a/frappe/tests/ui/test_module_view.js +++ /dev/null @@ -1,41 +0,0 @@ -QUnit.module('views'); - -QUnit.test("Test modules view", function(assert) { - assert.expect(4); - let done = assert.async(); - - frappe.run_serially([ - - //click Document Share Report in Permissions section [Report] - () => frappe.set_route("modules", "Setup"), - () => frappe.timeout(0.5), - () => frappe.click_link('Document Share Report'), - () => assert.deepEqual(frappe.get_route(), ["List", "DocShare", "Report", "Document Share Report"], - 'document share report'), - - //click Print Setting in Printing section [Form] - () => frappe.set_route("modules", "Setup"), - () => frappe.timeout(0.5), - () => frappe.click_link('Print Settings'), - () => assert.deepEqual(frappe.get_route(), ["Form", "Print Settings"], - 'print settings'), - - //click Workflow Action in Workflow section [List] - () => frappe.set_route("modules", "Setup"), - () => frappe.timeout(0.5), - () => frappe.click_link('Workflow Action'), - () => assert.deepEqual(frappe.get_route(), ["List", "Workflow Action", "List"], - 'workflow action'), - - //click Workflow Action in Workflow section [List] - () => frappe.set_route("modules"), - () => frappe.timeout(0.5), - () => frappe.click_link('Tools'), - () => frappe.timeout(0.5), - () => frappe.click_link('To Do'), - () => assert.deepEqual(frappe.get_route(), ["List", "ToDo", "List"], - 'todo list'), - - () => done() - ]); -}); diff --git a/frappe/tests/ui/test_number_format.js b/frappe/tests/ui/test_number_format.js deleted file mode 100644 index 2bca7d92f6..0000000000 --- a/frappe/tests/ui/test_number_format.js +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -// MIT License. See license.txt - -QUnit.module("Number Formatting"); - -QUnit.test("#,###.##", function(assert) { - assert.equal(format_number(100, "#,###.##"), "100.00"); - assert.equal(format_number(1000, "#,###.##"), "1,000.00"); - assert.equal(format_number(10000, "#,###.##"), "10,000.00"); - assert.equal(format_number(1000000, "#,###.##"), "1,000,000.00"); - assert.equal(format_number(1000000.345, "#,###.##"), "1,000,000.35"); -}); - -QUnit.test("#,##,###.##", function(assert) { - assert.equal(format_number(100, "#,##,###.##"), "100.00"); - assert.equal(format_number(1000, "#,##,###.##"), "1,000.00"); - assert.equal(format_number(10000, "#,##,###.##"), "10,000.00"); - assert.equal(format_number(1000000, "#,##,###.##"), "10,00,000.00"); - assert.equal(format_number(1000000.341, "#,##,###.##"), "10,00,000.34"); - assert.equal(format_number(10000000.341, "#,##,###.##"), "1,00,00,000.34"); -}); - -QUnit.test("#.###,##", function(assert) { - assert.equal(format_number(100, "#.###,##"), "100,00"); - assert.equal(format_number(1000, "#.###,##"), "1.000,00"); - assert.equal(format_number(10000, "#.###,##"), "10.000,00"); - assert.equal(format_number(1000000, "#.###,##"), "1.000.000,00"); - assert.equal(format_number(1000000.345, "#.###,##"), "1.000.000,35"); -}); - -QUnit.test("#.###", function(assert) { - assert.equal(format_number(100, "#.###"), "100"); - assert.equal(format_number(1000, "#.###"), "1.000"); - assert.equal(format_number(10000, "#.###"), "10.000"); - assert.equal(format_number(-100000, "#.###"), "-100.000"); - assert.equal(format_number(1000000, "#.###"), "1.000.000"); - assert.equal(format_number(1000000.345, "#.###"), "1.000.000"); -}); \ No newline at end of file diff --git a/frappe/tests/ui/test_social_login_key_buttons.py b/frappe/tests/ui/test_social_login_key_buttons.py deleted file mode 100644 index 376d4139e4..0000000000 --- a/frappe/tests/ui/test_social_login_key_buttons.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt -from __future__ import unicode_literals - -import unittest, frappe, time -from frappe.utils.selenium_testdriver import TestDriver - -class TestSocialLoginKeyButtons(unittest.TestCase): - def setUp(self): - try: - frappe_login_key = frappe.get_doc("Social Login Key", "frappe") - except frappe.DoesNotExistError: - frappe_login_key = frappe.new_doc("Social Login Key") - frappe_login_key.get_social_login_provider("Frappe", initialize=True) - frappe_login_key.base_url = "http://localhost:8000" - frappe_login_key.enable_social_login = 1 - frappe_login_key.client_id = "test_client_id" - frappe_login_key.client_secret = "test_client_secret" - frappe_login_key.save() - - self.driver = TestDriver() - - def test_login_buttons(self): - - # Go to Login Page - self.driver.get("login") - - time.sleep(2) - frappe_social_login = self.driver.find(".btn-frappe") - self.assertTrue(len(frappe_social_login) > 0) - - def tearDown(self): - self.driver.close() diff --git a/frappe/tests/ui/test_test_runner.py b/frappe/tests/ui/test_test_runner.py deleted file mode 100644 index 8b207cd02d..0000000000 --- a/frappe/tests/ui/test_test_runner.py +++ /dev/null @@ -1,84 +0,0 @@ -from __future__ import print_function, unicode_literals -from frappe.utils.selenium_testdriver import TestDriver -import unittest, os, frappe, time - -class TestTestRunner(unittest.TestCase): - def test_test_runner(self): - if frappe.flags.run_setup_wizard_ui_test: - for setup_wizard_test in frappe.get_hooks("setup_wizard_test"): - passed = frappe.get_attr(setup_wizard_test)() - self.assertTrue(passed) - return - - driver = TestDriver() - frappe.db.set_default('in_selenium', '1') - driver.login() - for test in get_tests(): - if test.startswith('#'): - continue - - timeout = 60 - passed = False - if '#' in test: - test, comment = test.split('#') - test = test.strip() - if comment.strip()=='long': - timeout = 300 - - print('Running {0}...'.format(test)) - - frappe.db.set_value('Test Runner', None, 'module_path', test) - frappe.db.commit() - driver.refresh() - driver.set_route('Form', 'Test Runner') - try: - driver.click_primary_action() - driver.wait_for('#frappe-qunit-done', timeout=timeout) - console = driver.get_console() - passed = 'Tests Passed' in console - finally: - console = driver.get_console() - passed = 'Test Passed' in console - if frappe.flags.tests_verbose or not passed: - for line in console: - print(line) - print('-' * 40) - else: - self.assertTrue(passed) - time.sleep(1) - frappe.db.set_default('in_selenium', None) - driver.close() - -def get_tests(): - '''Get tests base on flag''' - frappe.db.set_value('Test Runner', None, 'app', frappe.flags.ui_test_app or '') - - if frappe.flags.ui_test_list: - # list of tests - return get_tests_for(test_list=frappe.flags.ui_test_list) - elif frappe.flags.ui_test_path: - # specific test - return (frappe.flags.ui_test_path,) - elif frappe.flags.ui_test_app: - # specific app - return get_tests_for(frappe.flags.ui_test_app) - else: - # all apps - tests = [] - for app in frappe.get_installed_apps(): - tests.extend(get_tests_for(app)) - return tests - -def get_tests_for(app=None, test_list=None): - tests = [] - if test_list: - # Get all tests from a particular txt file - app, test_list = test_list.split(os.path.sep, 1) - tests_path = frappe.get_app_path(app, test_list) - else: - # Get all tests for a particular app - tests_path = frappe.get_app_path(app, 'tests', 'ui', 'tests.txt') - if os.path.exists(tests_path): - with open(tests_path, 'r') as fileobj: - tests = fileobj.read().strip().splitlines() - return tests diff --git a/frappe/tests/ui/test_todo.py b/frappe/tests/ui/test_todo.py deleted file mode 100644 index bb367e7851..0000000000 --- a/frappe/tests/ui/test_todo.py +++ /dev/null @@ -1,50 +0,0 @@ -from __future__ import print_function, unicode_literals -from frappe.utils.selenium_testdriver import TestDriver -import unittest -import time, os - -class TestToDo(unittest.TestCase): - def setUp(self): - self.driver = TestDriver() - - def test_todo(self): - self.driver.login() - - # list view - self.driver.set_route('List', 'ToDo') - - time.sleep(2) - - # new - self.driver.click_primary_action() - - time.sleep(2) - - # set input - self.driver.set_text_editor('description', 'hello') - - # save - self.driver.click_modal_primary_action() - - time.sleep(2) - - # refresh - self.driver.click_secondary_action() - - time.sleep(2) - - result_list = self.driver.get_visible_element('.result-list') - first_element_text = (result_list - .find_element_by_css_selector('.list-item') - .find_element_by_css_selector('.list-id').text) - - # if os.environ.get('CI'): - # # we don't run this test in Travis as it always fails - # # reinforcing why we use Unit Testing instead of integration - # # testing - # return - - self.assertTrue('hello' in first_element_text) - - def tearDown(self): - self.driver.close() diff --git a/frappe/tests/ui/tests.txt b/frappe/tests/ui/tests.txt deleted file mode 100644 index 2fcbf64851..0000000000 --- a/frappe/tests/ui/tests.txt +++ /dev/null @@ -1,20 +0,0 @@ -frappe/tests/ui/test_number_format.js -frappe/tests/ui/test_list/test_list_filter.js -frappe/tests/ui/test_list/test_list_paging.js -frappe/tests/ui/test_module_view.js -frappe/tests/ui/test_calendar_view.js -frappe/tests/ui/test_kanban/test_kanban_creation.js -frappe/tests/ui/test_kanban/test_kanban_view.js -frappe/tests/ui/test_kanban/test_kanban_filters.js -frappe/tests/ui/test_kanban/test_kanban_column.js -frappe/core/doctype/report/test_query_report.js -frappe/tests/ui/test_linked_with.js -frappe/custom/doctype/customize_form/test_customize_form.js -frappe/desk/doctype/event/test_event.js -frappe/tests/ui/test_control_html.js -frappe/tests/ui/test_control_geolocation.js -frappe/core/doctype/role_profile/test_role_profile.js -frappe/core/doctype/user/test_user_with_role_profile.js -frappe/tests/ui/test_list_count.js -frappe/workflow/doctype/workflow/tests/test_workflow_create.js -frappe/workflow/doctype/workflow/tests/test_workflow_test.js \ No newline at end of file diff --git a/frappe/twofactor.py b/frappe/twofactor.py index a539532f25..e60113215b 100644 --- a/frappe/twofactor.py +++ b/frappe/twofactor.py @@ -248,9 +248,7 @@ def get_link_for_qrcode(user, totp_uri): key = frappe.generate_hash(length=20) key_user = "{}_user".format(key) key_uri = "{}_uri".format(key) - lifespan = int(frappe.db.get_value('System Settings', 'System Settings', 'lifespan_qrcode_image')) - if lifespan<=0: - lifespan = 240 + lifespan = int(frappe.db.get_value('System Settings', 'System Settings', 'lifespan_qrcode_image')) or 240 frappe.cache().set_value(key_uri, totp_uri, expires_in_sec=lifespan) frappe.cache().set_value(key_user, user, expires_in_sec=lifespan) return get_url('/qrcode?k={}'.format(key)) @@ -387,7 +385,7 @@ def should_remove_barcode_image(barcode): '''Check if it's time to delete barcode image from server. ''' if isinstance(barcode, string_types): barcode = frappe.get_doc('File', barcode) - lifespan = frappe.db.get_value('System Settings', 'System Settings', 'lifespan_qrcode_image') + lifespan = frappe.db.get_value('System Settings', 'System Settings', 'lifespan_qrcode_image') or 240 if time_diff_in_seconds(get_datetime(), barcode.creation) > int(lifespan): return True return False diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 29c9387248..82e6ea1b45 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -679,3 +679,16 @@ def create_batch(iterable, batch_size): total_count = len(iterable) for i in range(0, total_count, batch_size): yield iterable[i:min(i + batch_size, total_count)] + +def set_request(**kwargs): + from werkzeug.test import EnvironBuilder + from werkzeug.wrappers import Request + builder = EnvironBuilder(**kwargs) + frappe.local.request = Request(builder.get_environ()) + +def get_html_for_route(route): + from frappe.website import render + set_request(method='GET', path=route) + response = render.render() + html = frappe.safe_decode(response.get_data()) + return html \ No newline at end of file diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py index ef345c67df..c1ac7581dc 100755 --- a/frappe/utils/background_jobs.py +++ b/frappe/utils/background_jobs.py @@ -79,8 +79,6 @@ def run_doc_method(doctype, name, doc_method, **kwargs): def execute_job(site, method, event, job_name, kwargs, user=None, is_async=True, retry=0): '''Executes job in a worker, performs commit/rollback and logs if there is any error''' - from frappe.utils.scheduler import log - if is_async: frappe.connect(site) if os.environ.get('CI'): @@ -115,12 +113,14 @@ def execute_job(site, method, event, job_name, kwargs, user=None, is_async=True, is_async=is_async, retry=retry+1) else: - log(method_name, message=repr(locals())) + frappe.log_error(method_name) raise except: frappe.db.rollback() - log(method_name, message=repr(locals())) + frappe.log_error(method_name) + frappe.db.commit() + print(frappe.get_traceback()) raise else: @@ -142,8 +142,6 @@ def start_worker(queue=None, quiet = False): with Connection(redis_connection): queues = get_queue_list(queue) logging_level = "INFO" - if quiet: - logging_level = "WARNING" Worker(queues, name=get_worker_name(queue)).work(logging_level = logging_level) def get_worker_name(queue): @@ -162,18 +160,25 @@ def get_worker_name(queue): def get_jobs(site=None, queue=None, key='method'): '''Gets jobs per queue or per site or both''' jobs_per_site = defaultdict(list) + + def add_to_dict(job): + if key in job.kwargs: + jobs_per_site[job.kwargs['site']].append(job.kwargs[key]) + + elif key in job.kwargs.get('kwargs', {}): + # optional keyword arguments are stored in 'kwargs' of 'kwargs' + jobs_per_site[job.kwargs['site']].append(job.kwargs['kwargs'][key]) + for queue in get_queue_list(queue): q = get_queue(queue) for job in q.jobs: if job.kwargs.get('site'): if site is None: - # get jobs for all sites - jobs_per_site[job.kwargs['site']].append(job.kwargs[key]) + add_to_dict(job) elif job.kwargs['site'] == site: - # get jobs only for given site - jobs_per_site[site].append(job.kwargs[key]) + add_to_dict(job) else: print('No site found in job', job.__dict__) @@ -198,13 +203,7 @@ def get_queue_list(queue_list=None): def get_queue(queue, is_async=True): '''Returns a Queue object tied to a redis connection''' validate_queue(queue) - - kwargs = { - 'connection': get_redis_conn(), - 'async': is_async - } - - return Queue(queue, **kwargs) + return Queue(queue, connection=get_redis_conn(), is_async=is_async) def validate_queue(queue, default_queue_list=None): if not default_queue_list: diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 8e407736bd..77d43b3778 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -681,13 +681,13 @@ def pretty_date(iso_datetime): else: return '{0} years ago'.format(cint(math.floor(dt_diff_days / 365.0))) -def comma_or(some_list): - return comma_sep(some_list, frappe._("{0} or {1}")) +def comma_or(some_list, add_quotes=True): + return comma_sep(some_list, frappe._("{0} or {1}"), add_quotes) -def comma_and(some_list): - return comma_sep(some_list, frappe._("{0} and {1}")) +def comma_and(some_list ,add_quotes=True): + return comma_sep(some_list, frappe._("{0} and {1}"), add_quotes) -def comma_sep(some_list, pattern): +def comma_sep(some_list, pattern, add_quotes=True): if isinstance(some_list, (list, tuple)): # list(some_list) is done to preserve the existing list some_list = [text_type(s) for s in list(some_list)] @@ -696,7 +696,7 @@ def comma_sep(some_list, pattern): elif len(some_list) == 1: return some_list[0] else: - some_list = ["'%s'" % s for s in some_list] + some_list = ["'%s'" % s for s in some_list] if add_quotes else ["%s" % s for s in some_list] return pattern.format(", ".join(frappe._(s) for s in some_list[:-1]), some_list[-1]) else: return some_list diff --git a/frappe/utils/global_search.py b/frappe/utils/global_search.py index 32f2f09326..48fd719f3b 100644 --- a/frappe/utils/global_search.py +++ b/frappe/utils/global_search.py @@ -302,7 +302,7 @@ def get_routes_to_index(): def add_route_to_global_search(route): from frappe.website.render import render_page - from frappe.tests.test_website import set_request + from frappe.utils import set_request frappe.set_user('Guest') frappe.local.no_cache = True diff --git a/frappe/utils/redis_wrapper.py b/frappe/utils/redis_wrapper.py index cb92ced5c6..4af59bceb2 100644 --- a/frappe/utils/redis_wrapper.py +++ b/frappe/utils/redis_wrapper.py @@ -43,7 +43,7 @@ class RedisWrapper(redis.Redis): try: if expires_in_sec: - self.setex(key, pickle.dumps(val), expires_in_sec) + self.setex(key, expires_in_sec, pickle.dumps(val)) else: self.set(key, pickle.dumps(val)) @@ -141,6 +141,9 @@ class RedisWrapper(redis.Redis): return super(RedisWrapper, self).llen(self.make_key(key)) def hset(self, name, key, value, shared=False): + if key is None: + return + _name = self.make_key(name, shared=shared) # set in local @@ -164,6 +167,8 @@ class RedisWrapper(redis.Redis): if not _name in frappe.local.cache: frappe.local.cache[_name] = {} + if not key: return None + if key in frappe.local.cache[_name]: return frappe.local.cache[_name][key] diff --git a/frappe/utils/scheduler.py b/frappe/utils/scheduler.py index b46036d996..7c0a642cb3 100755 --- a/frappe/utils/scheduler.py +++ b/frappe/utils/scheduler.py @@ -10,38 +10,15 @@ Events: from __future__ import unicode_literals, print_function -import frappe -import json +import frappe, os, time import schedule -import time -import frappe.utils -import os +from frappe.utils import now_datetime, get_datetime from frappe.utils import get_sites -from datetime import datetime -from frappe.utils.background_jobs import enqueue, get_jobs, queue_timeout -from frappe.utils.data import get_datetime, now_datetime from frappe.core.doctype.user.user import STANDARD_USERS -from frappe.installer import update_site_config -from six import string_types -from croniter import croniter +from frappe.utils.background_jobs import get_jobs DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S' -cron_map = { - "yearly": "0 0 1 1 *", - "annual": "0 0 1 1 *", - "monthly": "0 0 1 * *", - "monthly_long": "0 0 1 * *", - "weekly": "0 0 * * 0", - "weekly_long": "0 0 * * 0", - "daily": "0 0 * * *", - "daily_long": "0 0 * * *", - "midnight": "0 0 * * *", - "hourly": "0 * * * *", - "hourly_long": "0 * * * *", - "all": "0/" + str((frappe.get_conf().scheduler_interval or 240) // 60) + " * * * *", -} - def start_scheduler(): '''Run enqueue_events_for_all_sites every 2 minutes (default). Specify scheduler_interval in seconds in common_site_config.json''' @@ -60,17 +37,16 @@ def enqueue_events_for_all_sites(): return with frappe.init_site(): - jobs_per_site = get_jobs() sites = get_sites() for site in sites: try: - enqueue_events_for_site(site=site, queued_jobs=jobs_per_site[site]) + enqueue_events_for_site(site=site) except: # it should try to enqueue other sites print(frappe.get_traceback()) -def enqueue_events_for_site(site, queued_jobs): +def enqueue_events_for_site(site): def log_and_raise(): frappe.logger(__name__).error('Exception in Enqueue Events for Site {0}'.format(site) + '\n' + frappe.get_traceback()) @@ -82,7 +58,7 @@ def enqueue_events_for_site(site, queued_jobs): if is_scheduler_inactive(): return - enqueue_events(site=site, queued_jobs=queued_jobs) + enqueue_events(site=site) frappe.logger(__name__).debug('Queued events for site {0}'.format(site)) except frappe.db.OperationalError as e: @@ -96,128 +72,14 @@ def enqueue_events_for_site(site, queued_jobs): finally: frappe.destroy() -def enqueue_events(site, queued_jobs): - nowtime = frappe.utils.now_datetime() - last = frappe.db.get_value('System Settings', 'System Settings', 'scheduler_last_event') - - # set scheduler last event - frappe.db.set_value('System Settings', 'System Settings', - 'scheduler_last_event', nowtime.strftime(DATETIME_FORMAT), - update_modified=False) - frappe.db.commit() - - out = [] - if last: - last = datetime.strptime(last, DATETIME_FORMAT) - out = enqueue_applicable_events(site, nowtime, last, queued_jobs) - - return '\n'.join(out) - -def enqueue_applicable_events(site, nowtime, last, queued_jobs=()): - nowtime_str = nowtime.strftime(DATETIME_FORMAT) - out = [] - - enabled_events = get_enabled_scheduler_events() - - def trigger_if_enabled(site, event, last, queued_jobs): - trigger(site, event, last, queued_jobs) - _log(event) - - def _log(event): - out.append("{time} - {event} - queued".format(time=nowtime_str, event=event)) - - for event in enabled_events: - trigger_if_enabled(site, event, last, queued_jobs) - - if "all" not in enabled_events: - trigger_if_enabled(site, "all", last, queued_jobs) - - return out - -def trigger(site, event, last=None, queued_jobs=(), now=False): - """Trigger method in hooks.scheduler_events.""" - - queue = 'long' if event.endswith('_long') else 'short' - timeout = queue_timeout[queue] - if not queued_jobs and not now: - queued_jobs = get_jobs(site=site, queue=queue) - - if frappe.flags.in_test: - frappe.flags.ran_schedulers.append(event) - - events_from_hooks = get_scheduler_events(event) - if not events_from_hooks: - return - - events = events_from_hooks - if not now: - events = [] - if event == "cron": - for e in events_from_hooks: - e = cron_map.get(e, e) - if croniter.is_valid(e): - if croniter(e, last).get_next(datetime) <= frappe.utils.now_datetime(): - events.extend(events_from_hooks[e]) - else: - frappe.log_error("Cron string " + e + " is not valid", "Error triggering cron job") - frappe.logger(__name__).error('Exception in Trigger Events for Site {0}, Cron String {1}'.format(site, e)) - - else: - if croniter(cron_map[event], last).get_next(datetime) <= frappe.utils.now_datetime(): - events.extend(events_from_hooks) - - for handler in events: - if not now: - if handler not in queued_jobs: - enqueue(handler, queue, timeout, event) - else: - scheduler_task(site=site, event=event, handler=handler, now=True) - -def get_scheduler_events(event): - '''Get scheduler events from hooks and integrations''' - scheduler_events = frappe.cache().get_value('scheduler_events') - if not scheduler_events: - scheduler_events = frappe.get_hooks("scheduler_events") - frappe.cache().set_value('scheduler_events', scheduler_events) - - return scheduler_events.get(event) or [] - -def log(method, message=None): - """log error in patch_log""" - message = frappe.utils.cstr(message) + "\n" if message else "" - message += frappe.get_traceback() - - if not (frappe.db and frappe.db._conn): - frappe.connect() - - frappe.db.rollback() - frappe.db.begin() - - d = frappe.new_doc("Error Log") - d.method = method - d.error = message - d.insert(ignore_permissions=True) - - frappe.db.commit() - - return message - -def get_enabled_scheduler_events(): - if 'enabled_events' in frappe.flags and frappe.flags.enabled_events: - return frappe.flags.enabled_events - - enabled_events = frappe.db.get_global("enabled_scheduler_events") - if frappe.flags.in_test: - # TEMP for debug: this test fails randomly - print('found enabled_scheduler_events {0}'.format(enabled_events)) - - if enabled_events: - if isinstance(enabled_events, string_types): - enabled_events = json.loads(enabled_events) - return enabled_events - - return ["all", "hourly", "hourly_long", "daily", "daily_long", - "weekly", "weekly_long", "monthly", "monthly_long", "cron"] +def enqueue_events(site): + if schedule_jobs_based_on_activity(): + frappe.flags.enqueued_jobs = [] + queued_jobs = get_jobs(site=site, key='job_type').get(site) or [] + for job_type in frappe.get_all('Scheduled Job Type', ('name', 'method'), dict(stopped=0)): + if not job_type.method in queued_jobs: + # don't add it to queue if still pending + frappe.get_doc('Scheduled Job Type', job_type.name).enqueue() def is_scheduler_inactive(): if frappe.local.conf.maintenance_mode: @@ -246,97 +108,34 @@ def enable_scheduler(): def disable_scheduler(): toggle_scheduler(False) -def get_errors(from_date, to_date, limit): - errors = frappe.db.sql("""select modified, method, error from `tabError Log` - where date(modified) between %s and %s - and error not like '%%[Errno 110] Connection timed out%%' - order by modified limit %s""", (from_date, to_date, limit), as_dict=True) - return ["""

    Time: {modified}

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

    Error Logs (max {limit}):

    -

    URL: {url}


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

    No error logs

    " - -def scheduler_task(site, event, handler, now=False): - '''This is a wrapper function that runs a hooks.scheduler_events method''' - frappe.logger(__name__).info('running {handler} for {site} for event: {event}'.format(handler=handler, site=site, event=event)) - try: - if not now: - frappe.connect(site=site) - - frappe.flags.in_scheduler = True - frappe.get_attr(handler)() - - except Exception: - frappe.db.rollback() - traceback = log(handler, "Method: {event}, Handler: {handler}".format(event=event, handler=handler)) - frappe.logger(__name__).error(traceback) - raise - - else: - frappe.db.commit() - - frappe.logger(__name__).info('ran {handler} for {site} for event: {event}'.format(handler=handler, site=site, event=event)) - - -def reset_enabled_scheduler_events(login_manager): - if login_manager.info.user_type == "System User": - try: - if frappe.db.get_global('enabled_scheduler_events'): - # clear restricted events, someone logged in! - frappe.db.set_global('enabled_scheduler_events', None) - except frappe.db.InternalError as e: - if frappe.db.is_timedout(e): - frappe.log_error(frappe.get_traceback(), "Error in reset_enabled_scheduler_events") - else: - raise +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