diff --git a/.travis.yml b/.travis.yml index 30eb882256..9fab56188b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,6 +25,7 @@ cache: # https://docs.cypress.io/guides/guides/continuous-integration.html#Caching - ~/.cache + matrix: include: - name: "Python 3.7 MariaDB" @@ -46,7 +47,26 @@ matrix: script: bench --site test_site run-ui-tests frappe --headless before_install: - # install wkhtmltopdf + # do we really want to run travis? + - | + ONLY_DOCS_CHANGES=$(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '\.(md|png|jpg|jpeg)$|^.github|LICENSE' ; echo $?) + ONLY_JS_CHANGES=$(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '\.js$' ; echo $?) + ONLY_PY_CHANGES=$(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '\.py$' ; echo $?) + + if [[ $ONLY_DOCS_CHANGES == "1" ]]; then + echo "Only docs were updated, stopping build process."; + exit; + fi + if [[ $ONLY_JS_CHANGES == "1" && $TYPE == "server" ]]; then + echo "Only JavaScript code was updated; Stopping Python build process."; + exit; + fi + if [[ $ONLY_PY_CHANGES == "1" && $TYPE == "ui" ]]; then + echo "Only Python code was updated, stopping Cypress build process."; + exit; + fi + + # 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 diff --git a/frappe/__init__.py b/frappe/__init__.py index f0b6bfe41b..d917458510 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1559,10 +1559,10 @@ def get_doctype_app(doctype): loggers = {} log_level = None -def logger(module=None, with_more_info=True): +def logger(module=None, with_more_info=False): '''Returns a python logger that uses StreamHandler''' from frappe.utils.logger import get_logger - return get_logger(module or 'default', with_more_info=with_more_info) + return get_logger(module=module, with_more_info=with_more_info) def log_error(message=None, title=_("Error")): '''Log error to Error Log''' diff --git a/frappe/app.py b/frappe/app.py index 3bb764149b..50d09177d6 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -99,6 +99,16 @@ def application(request): frappe.monitor.stop(response) frappe.recorder.dump() + frappe.logger("web").info({ + "site": get_site_name(request.host), + "remote_addr": getattr(request, "remote_addr", "NOTFOUND"), + "base_url": getattr(request, "base_url", "NOTFOUND"), + "full_path": getattr(request, "full_path", "NOTFOUND"), + "method": getattr(request, "method", "NOTFOUND"), + "scheme": getattr(request, "scheme", "NOTFOUND"), + "http_status_code": getattr(response, "status_code", "NOTFOUND") + }) + if response and hasattr(frappe.local, 'rate_limiter'): response.headers.extend(frappe.local.rate_limiter.headers()) @@ -195,7 +205,6 @@ def handle_exception(e): frappe.local.login_manager.clear_cookies() if http_status_code >= 500: - frappe.logger().error('Request Error', exc_info=True) make_error_snapshot(e) if return_as_message: diff --git a/frappe/boot.py b/frappe/boot.py index e615cc49fa..695a4d754b 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -19,6 +19,7 @@ from frappe.email.inbox import get_email_accounts from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points +from frappe.model.base_document import get_controller from frappe.social.doctype.post.post import frequently_visited_links def get_bootinfo(): @@ -106,6 +107,7 @@ def load_desktop_data(bootinfo): from frappe.desk.desktop import get_desk_sidebar_items bootinfo.allowed_modules = get_modules_from_all_apps_for_user() bootinfo.allowed_workspaces = get_desk_sidebar_items(True) + bootinfo.module_page_map = get_controller("Desk Page").get_module_page_map() bootinfo.dashboards = frappe.get_all("Dashboard") def get_allowed_pages(cache=False): diff --git a/frappe/commands/site.py b/frappe/commands/site.py index b0abe9e2e6..163d25f183 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -447,7 +447,7 @@ def _drop_site(site, root_login='root', root_password=None, archived_sites_path= else: click.echo("="*80) click.echo("Error: The operation has stopped because backup of {s}'s database failed.".format(s=site)) - click.echo("Reason: {reason}{sep}".format(reason=err[1], sep="\n")) + click.echo("Reason: {reason}{sep}".format(reason=str(err), sep="\n")) click.echo("Fix the issue and try again.") click.echo( "Hint: Use 'bench drop-site {s} --force' to force the removal of {s}".format(sep="\n", tab="\t", s=site) diff --git a/frappe/core/doctype/access_log/test_access_log.py b/frappe/core/doctype/access_log/test_access_log.py index 312f77c026..9830507423 100644 --- a/frappe/core/doctype/access_log/test_access_log.py +++ b/frappe/core/doctype/access_log/test_access_log.py @@ -158,11 +158,7 @@ class TestAccessLog(unittest.TestCase): request = requests.post(private_file_link, headers=self.header) last_doc = frappe.get_last_doc('Access Log') - if request.status_code == 403: - # if file is not accessible, access log wont be generated - pass - - else: + if request.ok: # check for the access log of downloaded file self.assertEqual(new_private_file.doctype, last_doc.export_from) self.assertEqual(new_private_file.name, last_doc.reference_document) diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index d8f3bf9e77..20e4774add 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -259,7 +259,12 @@ class Communication(Document): # Timeline Links def set_timeline_links(self): - contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc]) + contacts = [] + if (self.email_account and frappe.db.get_value("Email Account", self.email_account, "create_contact")) or \ + frappe.flags.in_test: + + contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc]) + for contact_name in contacts: self.add_link('Contact', contact_name) diff --git a/frappe/core/doctype/communication/test_communication.py b/frappe/core/doctype/communication/test_communication.py index fb859586bb..6df90baaae 100644 --- a/frappe/core/doctype/communication/test_communication.py +++ b/frappe/core/doctype/communication/test_communication.py @@ -202,6 +202,8 @@ class TestCommunication(unittest.TestCase): self.assertIn(("Note", note.name), doc_links) def create_email_account(): + frappe.delete_doc_if_exists("Email Account", "_Test Comm Account 1") + frappe.flags.mute_emails = False frappe.flags.sent_mail = None diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py index c179054550..765ae5fe93 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -84,7 +84,7 @@ class ScheduledJobType(Document): def log_status(self, status): # log file - frappe.logger(__name__).info('Scheduled Job {0}: {1} for {2}'.format(status, self.method, frappe.local.site)) + frappe.logger("scheduler").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): diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py index 21679c5bc7..a24777a80a 100644 --- a/frappe/custom/doctype/custom_field/custom_field.py +++ b/frappe/custom/doctype/custom_field/custom_field.py @@ -46,6 +46,9 @@ class CustomField(Document): if not self.fieldname: frappe.throw(_("Fieldname not set for Custom Field")) + if self.fieldname in fieldnames: + frappe.throw(_("A field with the name '{}' already exists in doctype {}.").format(self.fieldname, self.dt)) + if self.get('translatable', 0) and not supports_translation(self.fieldtype): self.translatable = 0 diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js index 9053a9ab7f..a10d3d96f2 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js @@ -251,6 +251,7 @@ frappe.ui.form.on('Dashboard Chart', { render_filters_table: function(frm) { frm.set_df_property("filters_section", "hidden", 0); let is_document_type = frm.doc.chart_type!== 'Report' && frm.doc.chart_type!=='Custom'; + let is_dynamic_filter = f => ['Date', 'DateRange'].includes(f.fieldtype) && f.default; let wrapper = $(frm.get_field('filters_json').wrapper).empty(); let table = $(` @@ -268,6 +269,18 @@ frappe.ui.form.on('Dashboard Chart', { let filters = JSON.parse(frm.doc.filters_json || '[]'); var filters_set = false; + // Set dynamic filters for reports + if (frm.doc.chart_type == 'Report') { + let set_filters = false; + frm.chart_filters.forEach(f => { + if (is_dynamic_filter(f)) { + filters[f.fieldname] = f.default; + set_filters = true; + } + }); + set_filters && frm.set_value('filters_json', JSON.stringify(filters)); + } + let fields; if (is_document_type) { fields = [ @@ -292,6 +305,7 @@ frappe.ui.form.on('Dashboard Chart', { } } else if (frm.chart_filters.length) { fields = frm.chart_filters.filter(f => f.fieldname); + fields.map( f => { if (filters[f.fieldname]) { let condition = '='; @@ -318,7 +332,7 @@ frappe.ui.form.on('Dashboard Chart', { let dialog = new frappe.ui.Dialog({ title: __('Set Filters'), - fields: fields, + fields: fields.filter(f => !is_dynamic_filter(f)), primary_action: function() { let values = this.get_values(); if (values) { @@ -351,10 +365,15 @@ frappe.ui.form.on('Dashboard Chart', { } dialog.show(); - //Set query report object so that it can be used while fetching filter values in the report - frappe.query_report = new frappe.views.QueryReport({'filters': dialog.fields_list}); - frappe.query_reports[frm.doc.report_name].onload - && frappe.query_reports[frm.doc.report_name].onload(frappe.query_report); + + if (frm.doc.chart_type == 'Report') { + //Set query report object so that it can be used while fetching filter values in the report + frappe.query_report = new frappe.views.QueryReport({'filters': dialog.fields_list}); + frappe.query_reports[frm.doc.report_name] + && frappe.query_reports[frm.doc.report_name].onload + && frappe.query_reports[frm.doc.report_name].onload(frappe.query_report); + } + dialog.set_values(filters); }); }, diff --git a/frappe/desk/doctype/desk_page/desk_page.py b/frappe/desk/doctype/desk_page/desk_page.py index dd9cc0706a..f14535cb5f 100644 --- a/frappe/desk/doctype/desk_page/desk_page.py +++ b/frappe/desk/doctype/desk_page/desk_page.py @@ -20,6 +20,17 @@ class DeskPage(Document): if frappe.conf.developer_mode and self.is_standard: export_to_files(record_list=[['Desk Page', self.name]], record_module=self.module) + @staticmethod + def get_module_page_map(): + filters = { + 'extends_another_page': 0, + 'for_user': '', + } + + pages = frappe.get_all("Desk Page", fields=["name", "module"], filters=filters, as_list=1) + + return { page[1]: page[0] for page in pages } + def disable_saving_as_standard(): return frappe.flags.in_install or \ frappe.flags.in_patch or \ diff --git a/frappe/desk/doctype/notification_log/notification_log.js b/frappe/desk/doctype/notification_log/notification_log.js index 654b2b2b06..1f381d115b 100644 --- a/frappe/desk/doctype/notification_log/notification_log.js +++ b/frappe/desk/doctype/notification_log/notification_log.js @@ -3,10 +3,43 @@ frappe.ui.form.on('Notification Log', { refresh: function(frm) { - let dt = frm.doc.document_type; - let dn = frm.doc.document_name; - frm.fields_dict.document_name.$input_wrapper - .find('.control-value') - .wrapInner(``); + if (frm.doc.attached_file) { + frm.trigger('set_attachment'); + } else { + frm.get_field('attachment_link').$wrapper.empty(); + } + }, + + open_reference_document: function(frm) { + const dt = frm.doc.document_type; + const dn = frm.doc.document_name; + frappe.set_route('Form', dt, dn); + }, + + set_attachment: function(frm) { + const attachment = JSON.parse(frm.doc.attached_file); + + const $wrapper = frm.get_field('attachment_link').$wrapper; + $wrapper.html(` +
+
+ + ${attachment.name}.pdf +
+
+ `); + + $wrapper.find(".attached-file-link").click(() => { + const w = window.open( + frappe.urllib.get_full_url(`/api/method/frappe.utils.print_format.download_pdf? + doctype=${encodeURIComponent(attachment.doctype)} + &name=${encodeURIComponent(attachment.name)} + &format=${encodeURIComponent(attachment.print_format)} + &lang=${encodeURIComponent(attachment.lang)}`) + ); + if (!w) { + frappe.msgprint(__("Please enable pop-ups")); + } + }); } }); diff --git a/frappe/desk/doctype/notification_log/notification_log.json b/frappe/desk/doctype/notification_log/notification_log.json index ecb746df64..050bf85ead 100644 --- a/frappe/desk/doctype/notification_log/notification_log.json +++ b/frappe/desk/doctype/notification_log/notification_log.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2019-08-26 13:37:34.165254", "doctype": "DocType", "editable_grid": 1, @@ -8,10 +9,12 @@ "for_user", "type", "email_content", - "column_break_4", "document_type", "read", "document_name", + "attached_file", + "attachment_link", + "open_reference_document", "from_user" ], "fields": [ @@ -20,57 +23,65 @@ "fieldtype": "Text", "in_list_view": 1, "label": "Subject", - "read_only": 1 + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "for_user", "fieldtype": "Link", + "hidden": 1, "label": "For User", "options": "User", - "read_only": 1 + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "type", "fieldtype": "Select", + "hidden": 1, "in_list_view": 1, "in_standard_filter": 1, "label": "Type", - "options": "Mention\nEnergy Point\nAssignment\nShare", - "read_only": 1, - "search_index": 1 + "options": "Mention\nEnergy Point\nAssignment\nShare\nAlert", + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "email_content", - "fieldtype": "Text", - "label": "Email Content", - "read_only": 1 - }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" + "fieldtype": "Text Editor", + "label": "Message", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "document_type", "fieldtype": "Link", + "hidden": 1, "label": "Document Type", "options": "DocType", - "read_only": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "document_name", "fieldtype": "Data", - "label": "Document Name", - "read_only": 1, - "search_index": 1 + "hidden": 1, + "label": "Document Link", + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "from_user", "fieldtype": "Link", + "hidden": 1, "label": "From User", "options": "User", - "read_only": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -78,26 +89,51 @@ "fieldtype": "Check", "hidden": 1, "ignore_user_permissions": 1, - "label": "Read" + "label": "Read", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "open_reference_document", + "fieldtype": "Button", + "label": "Open Reference Document", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "attached_file", + "fieldtype": "Code", + "hidden": 1, + "label": "Attached File", + "options": "JSON", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "attachment_link", + "fieldtype": "HTML", + "label": "Attachment Link", + "show_days": 1, + "show_seconds": 1 } ], + "hide_toolbar": 1, "in_create": 1, - "modified": "2019-11-12 15:22:35.283678", + "links": [], + "modified": "2020-05-31 22:31:12.886950", "modified_by": "umair@erpnext.com", "module": "Desk", "name": "Notification Log", "owner": "Administrator", "permissions": [ { - "create": 1, "email": 1, "export": 1, "print": 1, "read": 1, "report": 1, "role": "All", - "share": 1, - "write": 1 + "share": 1 } ], "sort_field": "modified", diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py index 17eb6371b1..211b3ae5e6 100644 --- a/frappe/desk/doctype/notification_log/notification_log.py +++ b/frappe/desk/doctype/notification_log/notification_log.py @@ -48,6 +48,7 @@ def enqueue_create_notification(users, doc): if isinstance(users, frappe.string_types): users = [user.strip() for user in users.split(',') if user.strip()] + users = list(set(users)) frappe.enqueue( 'frappe.desk.doctype.notification_log.notification_log.make_notification_logs', @@ -58,6 +59,7 @@ def enqueue_create_notification(users, doc): def make_notification_logs(doc, users): from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled + for user in users: if frappe.db.exists('User', user): if is_notifications_enabled(user): @@ -68,7 +70,7 @@ def make_notification_logs(doc, users): _doc.update(doc) _doc.for_user = user _doc.subject = _doc.subject.replace('
', '').replace('
', '') - if _doc.for_user != _doc.from_user or doc.type == 'Energy Point': + if _doc.for_user != _doc.from_user or doc.type == 'Energy Point' or doc.type == 'Alert': _doc.insert(ignore_permissions=True) def send_notification_email(doc): diff --git a/frappe/desk/doctype/notification_settings/notification_settings.json b/frappe/desk/doctype/notification_settings/notification_settings.json index 6af325507b..85f93e156e 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.json +++ b/frappe/desk/doctype/notification_settings/notification_settings.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "Prompt", "creation": "2019-09-11 22:15:44.851526", "doctype": "DocType", @@ -21,52 +22,68 @@ "default": "1", "fieldname": "enabled", "fieldtype": "Check", - "label": "Enabled" + "label": "Enabled", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "subscribed_documents", "fieldtype": "Table MultiSelect", "label": "Subscribed Documents", - "options": "Notification Subscribed Document" + "options": "Notification Subscribed Document", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_3", "fieldtype": "Section Break", - "label": "Email Settings" + "label": "Email Settings", + "show_days": 1, + "show_seconds": 1 }, { "default": "1", "fieldname": "enable_email_notifications", "fieldtype": "Check", - "label": "Enable Email Notifications" + "label": "Enable Email Notifications", + "show_days": 1, + "show_seconds": 1 }, { "default": "1", "depends_on": "enable_email_notifications", "fieldname": "enable_email_mention", "fieldtype": "Check", - "label": "Mentions" + "label": "Mentions", + "show_days": 1, + "show_seconds": 1 }, { "default": "1", "depends_on": "enable_email_notifications", "fieldname": "enable_email_assignment", "fieldtype": "Check", - "label": "Assignments" + "label": "Assignments", + "show_days": 1, + "show_seconds": 1 }, { "default": "1", "depends_on": "enable_email_notifications", "fieldname": "enable_email_energy_point", "fieldtype": "Check", - "label": "Energy Points" + "label": "Energy Points", + "show_days": 1, + "show_seconds": 1 }, { "default": "1", "depends_on": "enable_email_notifications", "fieldname": "enable_email_share", "fieldtype": "Check", - "label": "Document Share" + "label": "Document Share", + "show_days": 1, + "show_seconds": 1 }, { "default": "__user", @@ -75,18 +92,23 @@ "hidden": 1, "label": "User", "options": "User", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "seen", "fieldtype": "Check", "hidden": 1, - "label": "Seen" + "label": "Seen", + "show_days": 1, + "show_seconds": 1 } ], "in_create": 1, - "modified": "2019-11-19 12:57:59.356786", + "links": [], + "modified": "2020-05-31 22:16:40.798019", "modified_by": "Administrator", "module": "Desk", "name": "Notification Settings", diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py index 6b5a13ee27..9b124cd6f4 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.py +++ b/frappe/desk/doctype/notification_settings/notification_settings.py @@ -28,6 +28,9 @@ def is_email_notifications_enabled_for_type(user, notification_type): if not is_email_notifications_enabled(user): return False + if notification_type == 'Alert': + return False + fieldname = 'enable_email_' + frappe.scrub(notification_type) enabled = frappe.db.get_value('Notification Settings', user, fieldname) if enabled is None: diff --git a/frappe/desk/page/user_profile/user_profile.js b/frappe/desk/page/user_profile/user_profile.js index c43ff27ba3..646c31f7a1 100644 --- a/frappe/desk/page/user_profile/user_profile.js +++ b/frappe/desk/page/user_profile/user_profile.js @@ -110,7 +110,11 @@ class UserProfile { render_line_chart() { - this.line_chart_filters = [['Energy Point Log', 'user', '=', this.user_id, false]]; + this.line_chart_filters = [ + ['Energy Point Log', 'user', '=', this.user_id, false], + ['Energy Point Log', 'type', '!=', 'Review', false] + ]; + this.line_chart_config = { timespan: 'Last Month', time_interval: 'Daily', @@ -186,7 +190,10 @@ class UserProfile { options: ['All', 'Auto', 'Criticism', 'Appreciation', 'Revert'], action: (selected_item) => { if (selected_item === 'All') { - if (this.line_chart_filters.length > 1) this.line_chart_filters.pop(); + this.line_chart_filters = [ + ['Energy Point Log', 'user', '=', this.user_id, false], + ['Energy Point Log', 'type', '!=', 'Review', false] + ]; } else { this.line_chart_filters[1] = ['Energy Point Log', 'type', '=', selected_item, false]; } diff --git a/frappe/email/doctype/email_account/email_account.json b/frappe/email/doctype/email_account/email_account.json index 6bde0291a0..057638697a 100644 --- a/frappe/email/doctype/email_account/email_account.json +++ b/frappe/email/doctype/email_account/email_account.json @@ -29,6 +29,7 @@ "default_incoming", "email_sync_option", "initial_sync_count", + "create_contact", "section_break_12", "enable_automatic_linking", "section_break_13", @@ -114,9 +115,9 @@ "depends_on": "eval:!doc.service", "fieldname": "domain", "fieldtype": "Link", - "label": "Domain", "in_list_view": 1, "in_standard_filter": 1, + "label": "Domain", "options": "Email Domain" }, { @@ -408,11 +409,17 @@ "fieldname": "use_ssl_for_outgoing", "fieldtype": "Check", "label": "Use SSL for Outgoing" + }, + { + "default": "1", + "fieldname": "create_contact", + "fieldtype": "Check", + "label": "Create Contacts from Incoming Emails" } ], "icon": "fa fa-inbox", "links": [], - "modified": "2020-04-06 19:20:50.491146", + "modified": "2020-05-11 15:18:43.931499", "modified_by": "Administrator", "module": "Email", "name": "Email Account", @@ -427,11 +434,11 @@ "write": 1 }, { - "read": 1, - "role": "Inbox User" + "read": 1, + "role": "Inbox User" } ], "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/email/doctype/notification/notification.js b/frappe/email/doctype/notification/notification.js index 44056955f7..02fc8512ca 100644 --- a/frappe/email/doctype/notification/notification.js +++ b/frappe/email/doctype/notification/notification.js @@ -80,7 +80,6 @@ frappe.ui.form.on("Notification", { }); }, refresh: function(frm) { - frm.toggle_reqd("recipients", frm.doc.channel=="Email"); frappe.notification.setup_fieldname_select(frm); frm.get_field("is_standard").toggle(frappe.boot.developer_mode); frm.trigger('event'); diff --git a/frappe/email/doctype/notification/notification.json b/frappe/email/doctype/notification/notification.json index 14eff2251a..d1526f5fe4 100644 --- a/frappe/email/doctype/notification/notification.json +++ b/frappe/email/doctype/notification/notification.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_rename": 1, "autoname": "Prompt", "creation": "2014-07-11 17:18:09.923399", @@ -22,6 +23,7 @@ "days_in_advance", "value_changed", "sender", + "send_system_notification", "sender_email", "section_break_9", "condition", @@ -46,32 +48,43 @@ "default": "1", "fieldname": "enabled", "fieldtype": "Check", - "label": "Enabled" + "label": "Enabled", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_2", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "Email", + "depends_on": "eval: !doc.disable_channel", "fieldname": "channel", "fieldtype": "Select", "label": "Channel", - "options": "Email\nSlack", + "options": "Email\nSlack\nSystem Notification", "reqd": 1, - "set_only_once": 1 + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.channel=='Slack'", "fieldname": "slack_webhook_url", "fieldtype": "Link", "label": "Slack Channel", - "options": "Slack Webhook URL" + "mandatory_depends_on": "eval:doc.channel=='Slack'", + "options": "Slack Webhook URL", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "filters", "fieldtype": "Section Break", - "label": "Filters" + "label": "Filters", + "show_days": 1, + "show_seconds": 1 }, { "description": "To add dynamic subject, use jinja tags like\n\n
{{ doc.name }} Delivered
", @@ -80,7 +93,9 @@ "ignore_xss_filter": 1, "in_list_view": 1, "label": "Subject", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "document_type", @@ -90,13 +105,17 @@ "label": "Document Type", "options": "DocType", "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "is_standard", "fieldtype": "Check", - "label": "Is Standard" + "label": "Is Standard", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "is_standard", @@ -104,11 +123,15 @@ "fieldtype": "Link", "in_standard_filter": 1, "label": "Module", - "options": "Module Def" + "options": "Module Def", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break_1", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "event", @@ -117,21 +140,27 @@ "label": "Send Alert On", "options": "\nNew\nSave\nSubmit\nCancel\nDays After\nDays Before\nValue Change\nMethod\nCustom", "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.event=='Method'", "description": "Trigger on valid methods like \"before_insert\", \"after_update\", etc (will depend on the DocType selected)", "fieldname": "method", "fieldtype": "Data", - "label": "Trigger Method" + "label": "Trigger Method", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.event==\"Days After\" || doc.event==\"Days Before\"", "description": "Send alert if date matches this field's value", "fieldname": "date_changed", "fieldtype": "Select", - "label": "Reference Date" + "label": "Reference Date", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -139,31 +168,41 @@ "description": "Send days before or after the reference date", "fieldname": "days_in_advance", "fieldtype": "Int", - "label": "Days Before or After" + "label": "Days Before or After", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.event==\"Value Change\"", "description": "Send alert if this field's value changes", "fieldname": "value_changed", "fieldtype": "Select", - "label": "Value Changed" + "label": "Value Changed", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sender", "fieldtype": "Link", "label": "Sender", - "options": "Email Account" + "options": "Email Account", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sender_email", "fieldtype": "Data", "label": "Sender Email", "options": "Email", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_9", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "description": "Optional: The alert will be sent if this expression is true", @@ -171,99 +210,143 @@ "fieldtype": "Code", "ignore_xss_filter": 1, "in_list_view": 1, - "label": "Condition" + "label": "Condition", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_6", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "html_7", "fieldtype": "HTML", - "options": "

Condition Examples:

\n
doc.status==\"Open\"
doc.due_date==nowdate()
doc.total > 40000\n
\n" + "options": "

Condition Examples:

\n
doc.status==\"Open\"
doc.due_date==nowdate()
doc.total > 40000\n
\n", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "property_section", "fieldtype": "Section Break", - "label": "Set Property After Alert" + "label": "Set Property After Alert", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "set_property_after_alert", "fieldtype": "Select", - "label": "Set Property After Alert" + "label": "Set Property After Alert", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "property_value", "fieldtype": "Data", - "label": "Value To Be Set" + "label": "Value To Be Set", + "show_days": 1, + "show_seconds": 1 }, { - "depends_on": "eval:doc.channel=='Email'", + "depends_on": "eval:doc.channel!=='Slack'", "fieldname": "column_break_5", "fieldtype": "Section Break", - "label": "Recipients" + "label": "Recipients", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "recipients", "fieldtype": "Table", "label": "Recipients", - "options": "Notification Recipient" + "mandatory_depends_on": "eval:doc.channel!=='Slack'", + "options": "Notification Recipient", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "message_sb", "fieldtype": "Section Break", - "label": "Message" + "label": "Message", + "show_days": 1, + "show_seconds": 1 }, { "default": "Add your message here", "fieldname": "message", "fieldtype": "Code", "ignore_xss_filter": 1, - "label": "Message" + "label": "Message", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.channel=='Email'", "fieldname": "message_examples", "fieldtype": "HTML", "label": "Message Examples", - "options": "
Message Example
\n\n
<h3>Order Overdue</h3>\n\n<p>Transaction {{ doc.name }} has exceeded Due Date. Please take necessary action.</p>\n\n<!-- show last comment -->\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n<h4>Details</h4>\n\n<ul>\n<li>Customer: {{ doc.customer }}\n<li>Amount: {{ doc.grand_total }}\n</ul>\n
" + "options": "
Message Example
\n\n
<h3>Order Overdue</h3>\n\n<p>Transaction {{ doc.name }} has exceeded Due Date. Please take necessary action.</p>\n\n<!-- show last comment -->\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n<h4>Details</h4>\n\n<ul>\n<li>Customer: {{ doc.customer }}\n<li>Amount: {{ doc.grand_total }}\n</ul>\n
", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.channel=='Slack'", "fieldname": "slack_message_examples", "fieldtype": "HTML", "label": "Message Examples", - "options": "
Message Example
\n\n
*Order Overdue*\n\nTransaction {{ doc.name }} has exceeded Due Date. Please take necessary action.\n\n\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n*Details*\n\n\u2022 Customer: {{ doc.customer }}\n\u2022 Amount: {{ doc.grand_total }}\n
" + "options": "
Message Example
\n\n
*Order Overdue*\n\nTransaction {{ doc.name }} has exceeded Due Date. Please take necessary action.\n\n\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n*Details*\n\n\u2022 Customer: {{ doc.customer }}\n\u2022 Amount: {{ doc.grand_total }}\n
", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "view_properties", "fieldtype": "Button", - "label": "View Properties (via Customize Form)" + "label": "View Properties (via Customize Form)", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "collapsible_depends_on": "attach_print", "fieldname": "column_break_25", "fieldtype": "Section Break", - "label": "Print Settings" + "label": "Print Settings", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "attach_print", "fieldtype": "Check", - "label": "Attach Print" + "label": "Attach Print", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "attach_print", "fieldname": "print_format", "fieldtype": "Link", "label": "Print Format", - "options": "Print Format" + "options": "Print Format", + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0", + "depends_on": "eval: doc.channel !== 'System Notification'", + "description": "If enabled, the notification will show up in the notifications dropdown on the top right corner of the navigation bar.", + "fieldname": "send_system_notification", + "fieldtype": "Check", + "label": "Send System Notification", + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-envelope", - "modified": "2019-07-15 13:17:02.585013", + "links": [], + "modified": "2020-05-29 16:03:10.914526", "modified_by": "Administrator", "module": "Email", "name": "Notification", diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index 8c011ade65..8e53b50fa2 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -13,6 +13,7 @@ from frappe.utils.jinja import validate_template from frappe.modules.utils import export_module_json, get_doc_module from six import string_types from frappe.integrations.doctype.slack_webhook_url.slack_webhook_url import send_slack_message +from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification class Notification(Document): def onload(self): @@ -125,6 +126,9 @@ def get_context(context): if self.channel == 'Slack': self.send_a_slack_msg(doc, context) + if self.channel == 'System Notification' or self.send_system_notification: + self.create_system_notification(doc, context) + if self.set_property_after_alert: allow_update = True if doc.docstatus == 1 and not doc.meta.get_field(self.set_property_after_alert).allow_on_submit: @@ -143,6 +147,25 @@ def get_context(context): except Exception: frappe.log_error(title='Document update failed', message=frappe.get_traceback()) + def create_system_notification(self, doc, context): + subject = self.subject + if "{" in subject: + subject = frappe.render_template(self.subject, context) + + attachments = self.get_attachment(doc) + recipients, cc, bcc = self.get_list_of_recipients(doc, context) + users = recipients + cc + bcc + + notification_doc = { + 'type': 'Alert', + 'document_type': doc.doctype, + 'document_name': doc.name, + 'subject': subject, + 'email_content': frappe.render_template(self.message, context), + 'attached_file': attachments and json.dumps(attachments[0]) + } + enqueue_create_notification(users, notification_doc) + def send_an_email(self, doc, context): from email.utils import formataddr subject = self.subject @@ -228,8 +251,7 @@ def get_context(context): # ignoring attachment as draft and cancelled documents are not allowed to print status = "Draft" if doc.docstatus == 0 else "Cancelled" - frappe.throw(_("""Not allowed to attach {0} document, - please enable Allow Print For {0} in Print Settings""".format(status)), + frappe.throw(_("""Not allowed to attach {0} document, please enable Allow Print For {0} in Print Settings""").format(status), title=_("Error in Notification")) else: return [{ diff --git a/frappe/installer.py b/frappe/installer.py index 17e9bceed7..4fc19b282a 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -269,6 +269,7 @@ def make_site_dirs(): os.path.join(site_private_path, 'backups'), os.path.join(site_public_path, 'files'), os.path.join(site_private_path, 'files'), + os.path.join(frappe.local.site_path, 'logs'), os.path.join(frappe.local.site_path, 'task-logs')): if not os.path.exists(dir_path): os.makedirs(dir_path) diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py index 2a036f4838..4b595b1abf 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py @@ -90,7 +90,7 @@ def backup_to_dropbox(upload_db_backup=True): dropbox_settings['access_token'] = access_token['oauth2_token'] set_dropbox_access_token(access_token['oauth2_token']) - dropbox_client = dropbox.Dropbox(dropbox_settings['access_token']) + dropbox_client = dropbox.Dropbox(dropbox_settings['access_token'], timeout=None) if upload_db_backup: if frappe.flags.create_new_backup: diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py index 5874c79108..6455623281 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.py +++ b/frappe/integrations/doctype/google_contacts/google_contacts.py @@ -147,11 +147,14 @@ def sync_contacts_from_google_contacts(g_contact): results = [] contacts_updated = 0 + sync_token = account.get_password(fieldname="next_sync_token", raise_exception=False) or None + contacts = frappe._dict() + while True: try: - sync_token = account.get_password(fieldname="next_sync_token", raise_exception=False) or None - contacts = google_contacts.people().connections().list(resourceName='people/me',syncToken=sync_token, - personFields="names,emailAddresses,organizations,phoneNumbers").execute() + contacts = google_contacts.people().connections().list(resourceName='people/me', pageToken=contacts.get("nextPageToken"), + syncToken=sync_token, pageSize=2000, requestSyncToken=True, personFields="names,emailAddresses,organizations,phoneNumbers").execute() + except HttpError as err: frappe.throw(_("Google Contacts - Could not sync contacts from Google Contacts {0}, error code {1}.").format(account.name, err.resp.status)) diff --git a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py index 5e464d4882..1d2f7f9495 100644 --- a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py +++ b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py @@ -64,6 +64,8 @@ from __future__ import unicode_literals import frappe from frappe import _ import json +import hmac +import hashlib from six.moves.urllib.parse import urlencode from frappe.model.document import Document from frappe.utils import get_url, call_hook_method, cint, get_timestamp @@ -317,6 +319,20 @@ class RazorpaySettings(Document): except Exception: frappe.log_error(frappe.get_traceback()) + def verify_signature(self, body, signature, key): + key = bytes(key, 'utf-8') + body = bytes(body, 'utf-8') + + dig = hmac.new(key=key, msg=body, digestmod=hashlib.sha256) + + generated_signature = dig.hexdigest() + result = hmac.compare_digest(generated_signature, signature) + + if not result: + frappe.throw(_('Razorpay Signature Verification Failed'), exc=frappe.PermissionError) + + return result + def capture_payment(is_sandbox=False, sanbox_response=None): """ Verifies the purchase as complete by the merchant. diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index feeb96898a..106d21eb51 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -693,7 +693,7 @@ class BaseDocument(object): df = self.meta.get_field(fieldname) sanitized_value = value - if df and df.get("fieldtype") in ("Data", "Code", "Small Text") and df.get("options")=="Email": + if df and df.get("fieldtype") in ("Data", "Code", "Small Text", "Text") and df.get("options")=="Email": sanitized_value = sanitize_email(value) elif df and (df.get("ignore_xss_filter") diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py index 4384e7c8f5..ea563dfc13 100644 --- a/frappe/model/workflow.py +++ b/frappe/model/workflow.py @@ -299,6 +299,7 @@ def set_workflow_state_on_action(doc, workflow_name, action): return action_map = { + 'update_after_submit': '1', 'submit': '1', 'cancel': '2' } diff --git a/frappe/patches.txt b/frappe/patches.txt index 0a7d368ee2..d8923ca320 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -278,6 +278,7 @@ frappe.patches.v13_0.set_path_for_homepage_in_web_page_view frappe.patches.v13_0.migrate_translation_column_data frappe.patches.v13_0.set_read_times frappe.patches.v13_0.remove_web_view +frappe.patches.v13_0.site_wise_logging frappe.patches.v13_0.set_unique_for_page_view frappe.patches.v13_0.remove_tailwind_from_page_builder frappe.patches.v13_0.rename_onboarding @@ -285,4 +286,4 @@ frappe.patches.v13_0.email_unsubscribe execute:frappe.delete_doc("Web Template", "Section with Left Image", force=1) execute:frappe.delete_doc("DocType", "Onboarding Slide") execute:frappe.delete_doc("DocType", "Onboarding Slide Field") -execute:frappe.delete_doc("DocType", "Onboarding Slide Help Link") \ No newline at end of file +execute:frappe.delete_doc("DocType", "Onboarding Slide Help Link") diff --git a/frappe/patches/v13_0/site_wise_logging.py b/frappe/patches/v13_0/site_wise_logging.py new file mode 100644 index 0000000000..6f04e0c9dd --- /dev/null +++ b/frappe/patches/v13_0/site_wise_logging.py @@ -0,0 +1,10 @@ +import os +import frappe + + +def execute(): + site = frappe.local.site + + log_folder = os.path.join(site, 'logs') + if not os.path.exists(log_folder): + os.mkdir(log_folder) \ No newline at end of file diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index bad7c877fc..369e4a56d4 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -650,13 +650,14 @@ frappe.ui.form.Form = class FrappeForm { frappe.utils.play_sound("submit"); callback && callback(); me.script_manager.trigger("on_submit") - .then(() => resolve(me)); - if (frappe.route_hooks.after_submit) { - let route_callback = frappe.route_hooks.after_submit; - delete frappe.route_hooks.after_submit; - - route_callback(me); - } + .then(() => resolve(me)) + .then(() => { + if (frappe.route_hooks.after_submit) { + let route_callback = frappe.route_hooks.after_submit; + delete frappe.route_hooks.after_submit; + route_callback(me); + } + }); } }, btn, () => me.handle_save_fail(btn, on_error), resolve); }); @@ -1586,7 +1587,7 @@ frappe.ui.form.Form = class FrappeForm { let steps = frappe.tour[this.doctype].map(step => { let field = this.get_docfield(step.fieldname); return { - element: `.frappe-control[title='${step.fieldname}']`, + element: `.frappe-control[data-fieldname='${step.fieldname}']`, popover: { title: step.title || field.label, description: step.description diff --git a/frappe/public/js/frappe/form/script_manager.js b/frappe/public/js/frappe/form/script_manager.js index fd5b8d3856..1238bf141c 100644 --- a/frappe/public/js/frappe/form/script_manager.js +++ b/frappe/public/js/frappe/form/script_manager.js @@ -16,12 +16,22 @@ frappe.ui.form.get_event_handler_list = function(doctype, fieldname) { frappe.ui.form.on = frappe.ui.form.on_change = function(doctype, fieldname, handler) { var add_handler = function(fieldname, handler) { var handler_list = frappe.ui.form.get_event_handler_list(doctype, fieldname); - handler_list.push(handler); + + let _handler = (...args) => { + try { + handler(...args); + } catch (error) { + console.error(handler); + throw error; + } + } + + handler_list.push(_handler); // add last handler to events so it can be called as // frm.events.handler(frm) if(cur_frm && cur_frm.doctype===doctype) { - cur_frm.events[fieldname] = handler; + cur_frm.events[fieldname] = _handler; } } diff --git a/frappe/public/js/frappe/socketio_client.js b/frappe/public/js/frappe/socketio_client.js index 9983a35779..1411b6289d 100644 --- a/frappe/public/js/frappe/socketio_client.js +++ b/frappe/public/js/frappe/socketio_client.js @@ -287,7 +287,8 @@ frappe.socketio.SocketIOUploader = class SocketIOUploader { } function fallback_required() { - return !frappe.boot.sysdefaults.use_socketio_to_upload_file || !frappe.socketio.socket.connected; + return !frappe.socketio.socket.connected + || !( !frappe.boot.sysdefaults || frappe.boot.sysdefaults.use_socketio_to_upload_file ); } if (fallback_required()) { diff --git a/frappe/public/js/frappe/ui/notifications/notifications.js b/frappe/public/js/frappe/ui/notifications/notifications.js index 2420d6772e..64d19ce63f 100644 --- a/frappe/public/js/frappe/ui/notifications/notifications.js +++ b/frappe/public/js/frappe/ui/notifications/notifications.js @@ -304,10 +304,7 @@ frappe.ui.Notifications = class Notifications { } get_dropdown_item_html(field) { - let doc_link = frappe.utils.get_form_link( - field.document_type, - field.document_name - ); + let doc_link = this.get_item_link(field); let read_class = field.read ? '' : 'unread'; let mark_read_action = field.read ? '': 'data-action="mark_as_read"'; let message = field.subject; @@ -336,6 +333,17 @@ frappe.ui.Notifications = class Notifications { return item_html; } + get_item_link(notification_doc) { + const link_doctype = + notification_doc.type == 'Alert' ? 'Notification Log': notification_doc.document_type; + const link_docname = + notification_doc.type == 'Alert' ? notification_doc.name: notification_doc.document_name; + return frappe.utils.get_form_link( + link_doctype, + link_docname + ); + } + render_dropdown_headers() { this.categories = [ { diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index f1677b5281..24fa946fc4 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -811,19 +811,19 @@ Object.assign(frappe.utils, { let total_duration = frappe.utils.seconds_to_duration(value, duration_options); if (total_duration.days) { - duration += total_duration.days + 'd'; + duration += total_duration.days + __('d', null, 'Days (Field: Duration)'); } if (total_duration.hours) { duration += (duration.length ? " " : ""); - duration += total_duration.hours + 'h'; + duration += total_duration.hours + __('h', null, 'Hours (Field: Duration)'); } if (total_duration.minutes) { duration += (duration.length ? " " : ""); - duration += total_duration.minutes + 'm'; + duration += total_duration.minutes + __('m', null, 'Minutes (Field: Duration)'); } if (total_duration.seconds) { duration += (duration.length ? " " : ""); - duration += total_duration.seconds + 's'; + duration += total_duration.seconds + __('s', null, 'Seconds (Field: Duration)'); } } return duration; @@ -998,4 +998,4 @@ String.prototype.plural = function(revert) { } return this; -}; \ No newline at end of file +}; diff --git a/frappe/public/js/frappe/utils/web_page_block.js b/frappe/public/js/frappe/utils/web_page_block.js deleted file mode 100644 index db655df98b..0000000000 --- a/frappe/public/js/frappe/utils/web_page_block.js +++ /dev/null @@ -1,38 +0,0 @@ -frappe.ui.form.on("Web Page Block", { - edit_values(frm, cdt, cdn) { - let row = frm.selected_doc; - frappe.model.with_doc("Web Template", row.web_template).then((doc) => { - let d = new frappe.ui.Dialog({ - title: __("Edit Values"), - fields: doc.fields.map((df) => { - if (df.fieldtype == "Section Break") { - df.collapsible = 1; - } - return df; - }), - primary_action(values) { - frappe.model.set_value( - cdt, - cdn, - "web_template_values", - JSON.stringify(values) - ); - d.hide(); - }, - }); - let values = JSON.parse(row.web_template_values || "{}"); - d.set_values(values); - d.show(); - - d.sections.forEach((sect) => { - let fields_with_value = sect.fields_list.filter( - (field) => values[field.df.fieldname] - ); - - if (fields_with_value.length) { - sect.collapse(false); - } - }); - }); - }, -}); \ No newline at end of file diff --git a/frappe/public/js/frappe/views/breadcrumbs.js b/frappe/public/js/frappe/views/breadcrumbs.js index 1c1049391f..0058310e3f 100644 --- a/frappe/public/js/frappe/views/breadcrumbs.js +++ b/frappe/public/js/frappe/views/breadcrumbs.js @@ -89,16 +89,21 @@ frappe.breadcrumbs = { breadcrumbs.module = frappe.breadcrumbs.module_map[breadcrumbs.module]; } - if(frappe.get_module(breadcrumbs.module)) { + let current_module = breadcrumbs.module + // Check if a desk page exists + if (frappe.boot.module_page_map[breadcrumbs.module]) { + breadcrumbs.module = frappe.boot.module_page_map[breadcrumbs.module]; + } + + if(frappe.get_module(current_module)) { // if module access exists - var module_info = frappe.get_module(breadcrumbs.module), + var module_info = frappe.get_module(current_module), icon = module_info && module_info.icon, label = module_info ? module_info.label : breadcrumbs.module; - if(module_info && !module_info.blocked && frappe.visible_modules.includes(module_info.module_name)) { $(repl('
  • %(label)s
  • ', - { module: breadcrumbs.module, label: __(label) })) + { module: breadcrumbs.module, label: __(breadcrumbs.module) })) .appendTo($breadcrumbs); } } diff --git a/frappe/public/js/frappe/widgets/chart_widget.js b/frappe/public/js/frappe/widgets/chart_widget.js index d32ca6f4d8..8a0eca9eaf 100644 --- a/frappe/public/js/frappe/widgets/chart_widget.js +++ b/frappe/public/js/frappe/widgets/chart_widget.js @@ -97,7 +97,6 @@ export default class ChartWidget extends Widget { this.chart_settings = {}; } this.setup_container(); - this.prepare_chart_object(); if (!this.in_customize_mode) { this.action_area.empty(); this.prepare_chart_actions(); @@ -110,7 +109,10 @@ export default class ChartWidget extends Widget { this.render_time_series_filters(); } } - this.fetch_and_update_chart(); + frappe.run_serially([ + () => this.prepare_chart_object(), + () => this.fetch_and_update_chart(), + ]); }); } @@ -625,13 +627,41 @@ export default class ChartWidget extends Widget { } prepare_chart_object() { - let saved_filters = this.chart_settings.filters || null; - this.filters = - saved_filters || this.filters || JSON.parse(this.chart_doc.filters_json || "[]"); - if (this.chart_doc.type == 'Heatmap' && !this.chart_doc.heatmap_year) { this.chart_doc.heatmap_year = frappe.dashboard_utils.get_year(frappe.datetime.now_date()); } + + return this.set_chart_filters(); + } + + set_chart_filters() { + let user_saved_filters = this.chart_settings.filters || null; + let chart_saved_filters = JSON.parse(this.chart_doc.filters_json || "null"); + + if (this.chart_doc.chart_type == 'Report') { + return frappe.dashboard_utils + .get_filters_for_chart_type(this.chart_doc).then(filters => { + chart_saved_filters = this.update_default_date_filters(filters, chart_saved_filters); + this.filters = + user_saved_filters || this.filters || chart_saved_filters; + }); + } else { + this.filters = + user_saved_filters || this.filters || chart_saved_filters; + return Promise.resolve(); + } + } + + update_default_date_filters(report_filters, chart_filters) { + report_filters.map(f => { + if (['Date', 'DateRange'].includes(f.fieldtype) && f.default) { + if (f.reqd || chart_filters[f.fieldname]) { + chart_filters[f.fieldname] = f.default; + } + } + }); + + return chart_filters; } get_settings() { diff --git a/frappe/public/js/frappe/widgets/shortcut_widget.js b/frappe/public/js/frappe/widgets/shortcut_widget.js index 572569839c..1de7e8bf75 100644 --- a/frappe/public/js/frappe/widgets/shortcut_widget.js +++ b/frappe/public/js/frappe/widgets/shortcut_widget.js @@ -29,9 +29,13 @@ export default class ShortcutWidget extends Widget { name: this.link_to, type: this.type, is_query_report: this.is_query_report, - doctype: this.ref_doctype + doctype: this.ref_doctype, }); + let filters = this.get_doctype_filter(); + if (this.type == "DocType" && filters) { + frappe.route_options = filters; + } frappe.set_route(route); }); } @@ -40,16 +44,26 @@ export default class ShortcutWidget extends Widget { if (this.in_customize_mode) return; this.widget.addClass("shortcut-widget-box"); - const get_filter = new Function(`return ${this.stats_filter}`); - if (this.type == "DocType" && this.stats_filter) { + + let filters = this.get_doctype_filter(); + if (this.type == "DocType" && filters) { frappe.db .count(this.link_to, { - filters: get_filter(), + filters: filters, }) .then((count) => this.set_count(count)); } } + get_doctype_filter() { + let count_filter = new Function(`return ${this.stats_filter}`)(); + if (count_filter) { + return count_filter; + } + + return null; + } + set_title() { if (this.icon) { this.title_field[0].innerHTML = `
    @@ -82,4 +96,4 @@ export default class ShortcutWidget extends Widget { buttons.appendTo(this.action_area); } -} +} \ No newline at end of file diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js index 5c44533b37..d5cd6d9643 100644 --- a/frappe/public/js/frappe/widgets/widget_dialog.js +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -241,11 +241,14 @@ class ShortcutDialog extends WidgetDialog { if (this.dialog.get_value("type") == "DocType" && this.filter_group) { let filters = this.filter_group.get_filters(); - filters.forEach((arr) => { - stats_filter[arr[1]] = [arr[2], arr[3]]; - }); - data.stats_filter = JSON.stringify(stats_filter); + if (filters.length) { + filters.forEach((arr) => { + stats_filter[arr[1]] = [arr[2], arr[3]]; + }); + + data.stats_filter = JSON.stringify(stats_filter); + } } data.label = data.label diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index f2e2319802..60179e98b4 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals, print_function from werkzeug.test import Client import os, re, sys, json, hashlib, requests, traceback +import functools from .html_utils import sanitize_html import frappe from frappe.utils.identicon import Identicon @@ -360,6 +361,7 @@ def decode_dict(d, encoding="utf-8"): return d +@functools.lru_cache() def get_site_name(hostname): return hostname.split(':')[0] diff --git a/frappe/utils/dashboard.py b/frappe/utils/dashboard.py index f06f9272b8..c1884d62fb 100644 --- a/frappe/utils/dashboard.py +++ b/frappe/utils/dashboard.py @@ -89,10 +89,14 @@ def sync_dashboards(app=None): config = get_config(app_name, module_name) if config: frappe.flags.in_import = True - make_records(config.charts, "Dashboard Chart") - make_records(config.number_cards, "Number Card") - make_records(config.dashboards, "Dashboard") - frappe.flags.in_import = False + try: + make_records(config.charts, "Dashboard Chart") + make_records(config.number_cards, "Number Card") + make_records(config.dashboards, "Dashboard") + except Exception as e: + frappe.log_error(e, _("Dashboard Import Error")) + finally: + frappe.flags.in_import = False def make_records(config, doctype): if not config: diff --git a/frappe/utils/error.py b/frappe/utils/error.py index c124410a7f..af8c0952e8 100644 --- a/frappe/utils/error.py +++ b/frappe/utils/error.py @@ -21,7 +21,7 @@ def make_error_snapshot(exception): if frappe.conf.disable_error_snapshot: return - logger = frappe.logger(__name__, with_more_info=False) + logger = frappe.logger(with_more_info=True) try: error_id = '{timestamp:s}-{ip:s}-{hash:s}'.format( diff --git a/frappe/utils/logger.py b/frappe/utils/logger.py index 5a77434cde..89e3711b0f 100755 --- a/frappe/utils/logger.py +++ b/frappe/utils/logger.py @@ -1,30 +1,53 @@ +# imports - compatibility imports from __future__ import unicode_literals -import frappe + +# imports - standard imports import logging +import os from logging.handlers import RotatingFileHandler + +# imports - third party imports from six import text_type -default_log_level = logging.DEBUG -LOG_FILENAME = '../logs/{}-frappe.log'.format(frappe.local.site) +# imports - module imports +import frappe -def get_logger(module, with_more_info=True): + +default_log_level = logging.DEBUG +site = getattr(frappe.local, 'site', None) + + +def get_logger(module, with_more_info=False): + global site if module in frappe.loggers: return frappe.loggers[module] - formatter = logging.Formatter('[%(levelname)s] %(asctime)s | %(pathname)s:\n%(message)s') - # handler = logging.StreamHandler() + if not module: + module = "frappe" + with_more_info = True - handler = RotatingFileHandler( - LOG_FILENAME, maxBytes=100000, backupCount=20) - handler.setFormatter(formatter) + logfile = module + '.log' + site = getattr(frappe.local, 'site', None) + LOG_FILENAME = os.path.join('..', 'logs', logfile) + + logger = logging.getLogger(module) + logger.setLevel(frappe.log_level or default_log_level) + logger.propagate = False + + formatter = logging.Formatter('%(asctime)s %(levelname)s %(name)s %(message)s') + handler = RotatingFileHandler(LOG_FILENAME, maxBytes=100_000, backupCount=20) + logger.addHandler(handler) +# + if site: + SITELOG_FILENAME = os.path.join(site, 'logs', logfile) + site_handler = RotatingFileHandler(SITELOG_FILENAME, maxBytes=100_000, backupCount=20) + site_handler.setFormatter(formatter) + logger.addHandler(site_handler) if with_more_info: handler.addFilter(SiteContextFilter()) - logger = logging.getLogger(module) - logger.setLevel(frappe.log_level or default_log_level) - logger.addHandler(handler) - logger.propagate = False + handler.setFormatter(formatter) frappe.loggers[module] = logger @@ -33,25 +56,9 @@ def get_logger(module, with_more_info=True): class SiteContextFilter(logging.Filter): """This is a filter which injects request information (if available) into the log.""" def filter(self, record): - record.msg = get_more_info_for_log() + text_type(record.msg) - return True - -def get_more_info_for_log(): - '''Adds Site, Form Dict into log entry''' - more_info = [] - site = getattr(frappe.local, 'site', None) - if site: - more_info.append('Site: {0}'.format(site)) - - form_dict = getattr(frappe.local, 'form_dict', None) - if form_dict: - more_info.append('Form Dict: {0}'.format(frappe.as_json(form_dict))) - - if more_info: - # to append a \n - more_info = more_info + [''] - - return '\n'.join(more_info) + if "Form Dict" not in text_type(record.msg): + record.msg = text_type(record.msg) + "\nSite: {0}\nForm Dict: {1}".format(site, getattr(frappe.local, 'form_dict', None)) + return True def set_log_level(level): '''Use this method to set log level to something other than the default DEBUG''' diff --git a/frappe/utils/scheduler.py b/frappe/utils/scheduler.py index 596595a160..749a41682f 100755 --- a/frappe/utils/scheduler.py +++ b/frappe/utils/scheduler.py @@ -7,17 +7,24 @@ Events: monthly weekly """ +# imports - compatibility imports +from __future__ import print_function, unicode_literals -from __future__ import unicode_literals, print_function +# imports - standard imports +import os +import time -import frappe, os, time +# imports - third party imports import schedule -from frappe.utils import now_datetime, get_datetime -from frappe.utils import get_sites -from frappe.installer import update_site_config + +# imports - module imports +import frappe from frappe.core.doctype.user.user import STANDARD_USERS +from frappe.installer import update_site_config +from frappe.utils import get_sites, now_datetime from frappe.utils.background_jobs import get_jobs + DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S' def start_scheduler(): @@ -48,9 +55,8 @@ def enqueue_events_for_all_sites(): 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()) - raise # pylint: disable=misplaced-bare-raise + error_message = 'Exception in Enqueue Events for Site {0}\n{1}'.format(site, frappe.get_traceback()) + frappe.logger("scheduler").error(error_message) try: frappe.init(site=site) @@ -60,10 +66,10 @@ def enqueue_events_for_site(site): enqueue_events(site=site) - frappe.logger(__name__).debug('Queued events for site {0}'.format(site)) + frappe.logger("scheduler").debug('Queued events for site {0}'.format(site)) except frappe.db.OperationalError as e: if frappe.db.is_access_denied(e): - frappe.logger(__name__).debug('Access denied for site {0}'.format(site)) + frappe.logger("scheduler").debug('Access denied for site {0}'.format(site)) else: log_and_raise() except: diff --git a/frappe/website/desk_page/website/website.json b/frappe/website/desk_page/website/website.json index c42a17d404..9936f8d7e2 100644 --- a/frappe/website/desk_page/website/website.json +++ b/frappe/website/desk_page/website/website.json @@ -38,10 +38,11 @@ "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "Website", - "modified": "2020-05-05 18:17:13.232473", + "modified": "2020-05-28 13:53:10.736212", "modified_by": "Administrator", "module": "Website", "name": "Website", @@ -51,7 +52,7 @@ "pin_to_top": 0, "shortcuts": [ { - "color": "", + "color": "#cef6d1", "format": "{} Published", "label": "Blog Post", "link_to": "Blog Post", @@ -59,6 +60,7 @@ "type": "DocType" }, { + "color": "#cef6d1", "format": "{} Active", "label": "Blogger", "link_to": "Blogger", @@ -66,8 +68,11 @@ "type": "DocType" }, { + "color": "#cef6d1", + "format": "{} Published", "label": "Web Page", "link_to": "Web Page", + "stats_filter": "{ \"published\": 1 }", "type": "DocType" }, { diff --git a/frappe/website/doctype/web_page/web_page.js b/frappe/website/doctype/web_page/web_page.js index 437a86b5d0..b2e06efc79 100644 --- a/frappe/website/doctype/web_page/web_page.js +++ b/frappe/website/doctype/web_page/web_page.js @@ -2,9 +2,6 @@ // MIT License. See license.txt frappe.ui.form.on('Web Page', { - onload: function() { - frappe.require('/assets/frappe/js/frappe/utils/web_page_block.js'); - }, title: function(frm) { if (frm.doc.title && !frm.doc.route) { frm.set_value('route', frappe.scrub(frm.doc.title, '-')); @@ -49,6 +46,45 @@ frappe.ui.form.on('Web Page', { } }); +frappe.ui.form.on("Web Page Block", { + edit_values(frm, cdt, cdn) { + let row = frm.selected_doc; + frappe.model.with_doc("Web Template", row.web_template).then((doc) => { + let d = new frappe.ui.Dialog({ + title: __("Edit Values"), + fields: doc.fields.map((df) => { + if (df.fieldtype == "Section Break") { + df.collapsible = 1; + } + return df; + }), + primary_action(values) { + frappe.model.set_value( + cdt, + cdn, + "web_template_values", + JSON.stringify(values) + ); + d.hide(); + }, + }); + let values = JSON.parse(row.web_template_values || "{}"); + d.set_values(values); + d.show(); + + d.sections.forEach((sect) => { + let fields_with_value = sect.fields_list.filter( + (field) => values[field.df.fieldname] + ); + + if (fields_with_value.length) { + sect.collapse(false); + } + }); + }); + }, +}); + frappe.tour['Web Page'] = [ { fieldname: "title", @@ -102,4 +138,4 @@ frappe.tour['Web Page'] = [ title: __("Meta Image"), description: __("The meta image is unique image representing the content of the page. Images for this Card should be at least 280px in width, and at least 150px in height.") }, -]; \ No newline at end of file +]; diff --git a/frappe/website/doctype/website_theme/website_theme.js b/frappe/website/doctype/website_theme/website_theme.js index 28b18a1bcd..75ecbe15e3 100644 --- a/frappe/website/doctype/website_theme/website_theme.js +++ b/frappe/website/doctype/website_theme/website_theme.js @@ -2,9 +2,6 @@ // MIT License. See license.txt frappe.ui.form.on('Website Theme', { - onload: function() { - frappe.require('/assets/frappe/js/frappe/utils/web_page_block.js'); - }, refresh(frm) { frm.clear_custom_buttons(); frm.toggle_display(["module", "custom"], !frappe.boot.developer_mode); diff --git a/frappe/website/module_onboarding/website/website.json b/frappe/website/module_onboarding/website/website.json index 606ef09811..c010d27eea 100644 --- a/frappe/website/module_onboarding/website/website.json +++ b/frappe/website/module_onboarding/website/website.json @@ -10,7 +10,7 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/website", "idx": 0, "is_complete": 0, - "modified": "2020-05-13 12:32:35.269025", + "modified": "2020-05-28 13:51:57.535269", "modified_by": "Administrator", "module": "Website", "name": "Website", diff --git a/frappe/website/onboarding_step/add_blog_category/add_blog_category.json b/frappe/website/onboarding_step/add_blog_category/add_blog_category.json index 5936e78c68..71924f8848 100644 --- a/frappe/website/onboarding_step/add_blog_category/add_blog_category.json +++ b/frappe/website/onboarding_step/add_blog_category/add_blog_category.json @@ -8,10 +8,12 @@ "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-04-30 19:06:10.750976", + "modified": "2020-05-28 13:51:53.157256", "modified_by": "Administrator", "name": "Add Blog Category", "owner": "Administrator", "reference_document": "Blog Category", - "title": "Add Blog Category" + "show_full_form": 0, + "title": "Add Blog Category", + "validate_action": 0 } \ No newline at end of file diff --git a/frappe/website/onboarding_step/create_blogger/create_blogger.json b/frappe/website/onboarding_step/create_blogger/create_blogger.json index 841325ca6a..48a0997025 100644 --- a/frappe/website/onboarding_step/create_blogger/create_blogger.json +++ b/frappe/website/onboarding_step/create_blogger/create_blogger.json @@ -13,5 +13,7 @@ "name": "Create Blogger", "owner": "Administrator", "reference_document": "Blogger", - "title": "Create Blogger" + "show_full_form": 0, + "title": "Create Blogger", + "validate_action": 0 } \ No newline at end of file diff --git a/frappe/website/onboarding_step/enable_website_tracking/enable_website_tracking.json b/frappe/website/onboarding_step/enable_website_tracking/enable_website_tracking.json index b37a704b1e..c445123243 100644 --- a/frappe/website/onboarding_step/enable_website_tracking/enable_website_tracking.json +++ b/frappe/website/onboarding_step/enable_website_tracking/enable_website_tracking.json @@ -14,6 +14,8 @@ "name": "Enable Website Tracking", "owner": "Administrator", "reference_document": "Website Settings", + "show_full_form": 0, "title": "Enable Website Tracking", + "validate_action": 0, "value_to_validate": "1" } \ No newline at end of file diff --git a/frappe/website/onboarding_step/introduction_to_website/introduction_to_website.json b/frappe/website/onboarding_step/introduction_to_website/introduction_to_website.json index f3c95657e8..29c1d8ff60 100644 --- a/frappe/website/onboarding_step/introduction_to_website/introduction_to_website.json +++ b/frappe/website/onboarding_step/introduction_to_website/introduction_to_website.json @@ -8,10 +8,12 @@ "is_mandatory": 1, "is_single": 0, "is_skipped": 0, - "modified": "2020-04-30 19:06:10.578218", + "modified": "2020-05-28 13:51:51.485924", "modified_by": "Administrator", "name": "Introduction to Website", "owner": "Administrator", + "show_full_form": 0, "title": "Introduction to Website", + "validate_action": 0, "video_url": "https://www.youtube.com/watch?v=lyW6mfFBSNw" } \ No newline at end of file diff --git a/frappe/website/onboarding_step/web_page_tour/web_page_tour.json b/frappe/website/onboarding_step/web_page_tour/web_page_tour.json index 1ee98d69ee..e73610d847 100644 --- a/frappe/website/onboarding_step/web_page_tour/web_page_tour.json +++ b/frappe/website/onboarding_step/web_page_tour/web_page_tour.json @@ -8,10 +8,12 @@ "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-05-13 12:32:15.966570", + "modified": "2020-05-28 13:51:54.264456", "modified_by": "Administrator", "name": "Web Page Tour", "owner": "Administrator", "reference_document": "Web Page", - "title": "Learn about Web Pages" + "show_full_form": 0, + "title": "Learn about Web Pages", + "validate_action": 0 } \ No newline at end of file diff --git a/frappe/website/website_theme/standard/standard.json b/frappe/website/website_theme/standard/standard.json index 11527a5229..62a5cc8d7b 100644 --- a/frappe/website/website_theme/standard/standard.json +++ b/frappe/website/website_theme/standard/standard.json @@ -7,9 +7,9 @@ "custom_overrides": "", "docstatus": 0, "doctype": "Website Theme", - "font_properties": "wght:400;500;600;700;800", - "idx": 28, - "modified": "2020-05-19 00:33:58.011046", + "font_properties": "400,500,600,700,800", + "idx": 26, + "modified": "2020-05-20 14:47:12.938879", "modified_by": "Administrator", "module": "Website", "name": "Standard",