diff --git a/frappe/__init__.py b/frappe/__init__.py index d644d2a473..c6e762919f 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -231,8 +231,7 @@ def get_site_config(sites_path=None, site_path=None): if os.path.exists(site_config): config.update(get_file_json(site_config)) elif local.site and not local.flags.new_site: - print("Site {0} does not exist".format(local.site)) - sys.exit(1) + raise IncorrectSitePath("{0} does not exist".format(local.site)) return _dict(config) @@ -436,12 +435,8 @@ def get_roles(username=None): """Returns roles of current user.""" if not local.session: return ["Guest"] - - if username: - import frappe.permissions - return frappe.permissions.get_roles(username) - else: - return get_user().get_roles() + import frappe.permissions + return frappe.permissions.get_roles(username or local.session.user) def get_request_header(key, default=None): """Return HTTP request header. @@ -1559,10 +1554,10 @@ def get_doctype_app(doctype): loggers = {} log_level = None -def logger(module=None, with_more_info=False): +def logger(module=None, with_more_info=False, allow_site=True, filter=None, max_size=100_000, file_count=20): '''Returns a python logger that uses StreamHandler''' from frappe.utils.logger import get_logger - return get_logger(module=module, with_more_info=with_more_info) + return get_logger(module=module, with_more_info=with_more_info, allow_site=allow_site, filter=filter, max_size=max_size, file_count=file_count) def log_error(message=None, title=_("Error")): '''Log error to Error Log''' @@ -1707,3 +1702,7 @@ def mock(type, size=1, locale='en'): from frappe.chat.util import squashify return squashify(results) + +def validate_and_sanitize_search_inputs(fn): + from frappe.desk.search import validate_and_sanitize_search_inputs as func + return func(fn) diff --git a/frappe/app.py b/frappe/app.py index 57db867882..725bec183a 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -99,7 +99,7 @@ def application(request): frappe.monitor.stop(response) frappe.recorder.dump() - frappe.logger("frappe.web").info({ + frappe.logger("frappe.web", allow_site=frappe.local.site).info({ "site": get_site_name(request.host), "remote_addr": getattr(request, "remote_addr", "NOTFOUND"), "base_url": getattr(request, "base_url", "NOTFOUND"), @@ -256,9 +256,11 @@ def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=No 'SERVER_NAME': 'localhost:8000' } + log = logging.getLogger('werkzeug') + log.propagate = False + in_test_env = os.environ.get('CI') if in_test_env: - log = logging.getLogger('werkzeug') log.setLevel(logging.ERROR) run_simple('0.0.0.0', int(port), application, diff --git a/frappe/auth.py b/frappe/auth.py index 64fea36748..998e97fe24 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -338,7 +338,7 @@ class CookieManager: self.set_cookie("country", frappe.session.session_country) def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"): - if not secure: + if not secure and hasattr(frappe.local, 'request'): secure = frappe.local.request.scheme == "https" self.cookies[key] = { "value": value, diff --git a/frappe/automation/desk_page/tools/tools.json b/frappe/automation/desk_page/tools/tools.json index 2164a4ce38..3fbaf62d02 100644 --- a/frappe/automation/desk_page/tools/tools.json +++ b/frappe/automation/desk_page/tools/tools.json @@ -3,7 +3,7 @@ { "hidden": 0, "label": "Tools", - "links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Video\",\n \"name\": \"Video\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]" + "links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]" }, { "hidden": 0, @@ -29,10 +29,11 @@ "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "Tools", - "modified": "2020-04-20 18:21:14.152537", + "modified": "2020-07-21 19:32:18.480700", "modified_by": "Administrator", "module": "Automation", "name": "Tools", diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index a946fcc81c..c09e347e71 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -374,6 +374,7 @@ def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, e # method for reference_doctype filter @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters): res = frappe.db.get_all('Property Setter', { 'property': 'allow_auto_repeat', diff --git a/frappe/contacts/address_and_contact.py b/frappe/contacts/address_and_contact.py index 51f13fb1a1..3ca9547188 100644 --- a/frappe/contacts/address_and_contact.py +++ b/frappe/contacts/address_and_contact.py @@ -147,6 +147,7 @@ def delete_contact_and_address(doctype, docname): doc.delete() @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, filters): if not txt: txt = "" diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py index 57dea8284c..e82ab9b26e 100644 --- a/frappe/contacts/doctype/address/address.py +++ b/frappe/contacts/doctype/address/address.py @@ -231,6 +231,7 @@ def get_company_address(company): return ret @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def address_query(doctype, txt, searchfield, start, page_len, filters): from frappe.desk.reportview import get_match_cond diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index 8240940d2f..f82946dc5e 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -183,6 +183,7 @@ def update_contact(doc, method): contact.save(ignore_permissions=True) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def contact_query(doctype, txt, searchfield, start, page_len, filters): from frappe.desk.reportview import get_match_cond diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 34e53488e5..485f7caf08 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -960,6 +960,9 @@ class Column: if not self.df: return + if self.skip_import: + return + if self.df.fieldtype == "Link": # find all values that dont exist values = list(set([cstr(v) for v in self.column_values[1:] if v])) diff --git a/frappe/core/doctype/report/report.js b/frappe/core/doctype/report/report.js index b4c30f5bbd..c410e9aa1a 100644 --- a/frappe/core/doctype/report/report.js +++ b/frappe/core/doctype/report/report.js @@ -1,6 +1,6 @@ frappe.ui.form.on('Report', { refresh: function(frm) { - if (frm.doc.is_standard && !frappe.boot.developer_mode) { + if (frm.doc.is_standard === "Yes" && !frappe.boot.developer_mode) { // make the document read-only frm.set_read_only(); } diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 64bff32189..ef2de679ae 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -812,6 +812,7 @@ def reset_password(user): return 'not found' @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def user_query(doctype, txt, searchfield, start, page_len, filters): from frappe.desk.reportview import get_match_cond diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py index 48d4fcb5d4..ba14583c2f 100644 --- a/frappe/core/doctype/user_permission/user_permission.py +++ b/frappe/core/doctype/user_permission/user_permission.py @@ -119,6 +119,8 @@ def user_permission_exists(user, allow, for_value, applicable_for=None): return has_same_user_permission +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_applicable_for_doctype_list(doctype, txt, searchfield, start, page_len, filters): linked_doctypes_map = get_linked_doctypes(doctype, True) diff --git a/frappe/core/doctype/video/__init__.py b/frappe/core/doctype/video/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/core/doctype/video/test_video.py b/frappe/core/doctype/video/test_video.py deleted file mode 100644 index 0bed1e98d6..0000000000 --- a/frappe/core/doctype/video/test_video.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies and Contributors -# See license.txt -from __future__ import unicode_literals - -# import frappe -import unittest - -class TestVideo(unittest.TestCase): - pass diff --git a/frappe/core/doctype/video/video.js b/frappe/core/doctype/video/video.js deleted file mode 100644 index 36ea240a36..0000000000 --- a/frappe/core/doctype/video/video.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2020, Frappe Technologies and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Video', { - // refresh: function(frm) { - - // } -}); diff --git a/frappe/core/doctype/video/video.json b/frappe/core/doctype/video/video.json deleted file mode 100644 index 26a407c05c..0000000000 --- a/frappe/core/doctype/video/video.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "actions": [], - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:title", - "creation": "2018-10-17 05:47:13.087395", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "title", - "provider", - "url", - "column_break_4", - "publish_date", - "duration", - "section_break_7", - "description" - ], - "fields": [ - { - "fieldname": "title", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Title", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "provider", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Provider", - "options": "YouTube\nVimeo", - "reqd": 1 - }, - { - "fieldname": "url", - "fieldtype": "Data", - "in_list_view": 1, - "label": "URL", - "reqd": 1 - }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, - { - "fieldname": "publish_date", - "fieldtype": "Date", - "label": "Publish Date" - }, - { - "fieldname": "duration", - "fieldtype": "Data", - "label": "Duration" - }, - { - "fieldname": "section_break_7", - "fieldtype": "Section Break" - }, - { - "fieldname": "description", - "fieldtype": "Text Editor", - "in_list_view": 1, - "label": "Description", - "reqd": 1 - } - ], - "links": [], - "modified": "2020-04-22 12:09:49.057403", - "modified_by": "Administrator", - "module": "Core", - "name": "Video", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "All", - "share": 1, - "write": 1 - }, - { - "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/core/doctype/video/video.py b/frappe/core/doctype/video/video.py deleted file mode 100644 index fdbd3a1abe..0000000000 --- a/frappe/core/doctype/video/video.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -# import frappe -from frappe.model.document import Document - -class Video(Document): - pass diff --git a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py index 8b2d1e01fa..97209cd8ea 100644 --- a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py +++ b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py @@ -41,6 +41,8 @@ def get_columns_and_fields(doctype): return columns, fields +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def query_doctypes(doctype, txt, searchfield, start, page_len, filters): user = filters.get("user") user_perms = frappe.utils.user.UserPermissions(user) diff --git a/frappe/data_migration/doctype/data_migration_run/test_data_migration_run.py b/frappe/data_migration/doctype/data_migration_run/test_data_migration_run.py index ba4a255b97..c6c3ea138c 100644 --- a/frappe/data_migration/doctype/data_migration_run/test_data_migration_run.py +++ b/frappe/data_migration/doctype/data_migration_run/test_data_migration_run.py @@ -108,7 +108,7 @@ def create_plan(): 'connector_name': 'Local Connector', 'connector_type': 'Frappe', # connect to same host. - 'hostname': frappe.conf.host_name, + 'hostname': frappe.conf.host_name or frappe.utils.get_site_url(frappe.local.site), 'username': 'Administrator', - 'password': 'admin' + 'password': frappe.conf.get("admin_password") or 'admin' }).insert(ignore_if_duplicate=True) diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index 68b57a93d4..ae9d070976 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -221,6 +221,8 @@ class Workspace: incomplete_dependencies = [d for d in item.dependencies if not _doctype_contains_a_record(d)] if len(incomplete_dependencies): item.incomplete_dependencies = incomplete_dependencies + else: + item.incomplete_dependencies = "" if item.onboard: # Mark Spotlights for initial diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js index 8d89cc2f31..7f26bd9101 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js @@ -23,7 +23,6 @@ frappe.ui.form.on('Dashboard Chart', { frm.chart_filters = null; if (!frappe.boot.developer_mode && frm.doc.is_standard) { - frm.set_df_property('chart_options_section', 'hidden', 1); frm.disable_form(); } @@ -57,11 +56,6 @@ frappe.ui.form.on('Dashboard Chart', { if (frm.doc.report_name) { frm.trigger('set_chart_report_filters'); } - - if (!frappe.boot.developer_mode) { - frm.set_df_property("custom_options", "hidden", 1); - } - }, is_standard: function(frm) { diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index a25e0cab9e..4ea61ec6a9 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -28,15 +28,28 @@ def get_permission_query_conditions(user): if "System Manager" in roles: return None - allowed_doctypes = ['"%s"' % doctype for doctype in frappe.permissions.get_doctypes_with_read()] - allowed_reports = ['"%s"' % key if type(key) == str else key.encode('UTF8') for key in get_allowed_reports()] + doctype_condition = False + report_condition = False + + allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()] + allowed_reports = [frappe.db.escape(key) if type(key) == str else key.encode('UTF8') for key in get_allowed_reports()] + + if allowed_doctypes: + doctype_condition = '`tabDashboard Chart`.`document_type` in ({allowed_doctypes})'.format( + allowed_doctypes=','.join(allowed_doctypes)) + if allowed_reports: + report_condition = '`tabDashboard Chart`.`report_name` in ({allowed_reports})'.format( + allowed_reports=','.join(allowed_reports)) return ''' - `tabDashboard Chart`.`document_type` in ({allowed_doctypes}) - or `tabDashboard Chart`.`report_name` in ({allowed_reports}) + (`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average') + and {doctype_condition}) + or + (`tabDashboard Chart`.`chart_type` = 'Report' + and {report_condition}) '''.format( - allowed_doctypes=','.join(allowed_doctypes), - allowed_reports=','.join(allowed_reports) + doctype_condition=doctype_condition, + report_condition=report_condition ) @@ -130,7 +143,7 @@ def add_chart_to_dashboard(args): dashboard_link = frappe.new_doc('Dashboard Chart Link') dashboard_link.chart = args.chart_name or args.name - if args.set_standard: + if args.set_standard and dashboard.is_standard: chart = frappe.get_doc('Dashboard Chart', dashboard_link.chart) chart.is_standard = 1 chart.module = dashboard.module @@ -344,6 +357,8 @@ def get_year_ending(date): # last day of this month return add_to_date(date, days=-1) +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_charts_for_user(doctype, txt, searchfield, start, page_len, filters): or_filters = {'owner': frappe.session.user, 'is_public': 1} return frappe.db.get_list('Dashboard Chart', diff --git a/frappe/desk/doctype/desk_page/desk_page.py b/frappe/desk/doctype/desk_page/desk_page.py index f14535cb5f..cc2db53481 100644 --- a/frappe/desk/doctype/desk_page/desk_page.py +++ b/frappe/desk/doctype/desk_page/desk_page.py @@ -5,14 +5,23 @@ from __future__ import unicode_literals import frappe from frappe import _ +from frappe.utils.data import validate_json_string from frappe.modules.export_file import export_to_files from frappe.model.document import Document class DeskPage(Document): def validate(self): + self.validate_cards_json() if (self.is_standard and not frappe.conf.developer_mode and not disable_saving_as_standard()): frappe.throw(_("You need to be in developer mode to edit this document")) + def validate_cards_json(self): + for card in self.cards: + try: + validate_json_string(card.links) + except frappe.ValidationError: + frappe.throw(_("Invalid JSON in card links for {0}").format(frappe.bold(card.label))) + def on_update(self): if disable_saving_as_standard(): return diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py index 3eb08ec00a..d4a2b00c57 100644 --- a/frappe/desk/doctype/number_card/number_card.py +++ b/frappe/desk/doctype/number_card/number_card.py @@ -32,13 +32,17 @@ def get_permission_query_conditions(user=None): if "System Manager" in roles: return None - allowed_doctypes = ['"%s"' % doctype for doctype in frappe.permissions.get_doctypes_with_read()] + doctype_condition = False + + allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()] + + if allowed_doctypes: + doctype_condition = '`tabNumber Card`.`document_type` in ({allowed_doctypes})'.format( + allowed_doctypes=','.join(allowed_doctypes)) return ''' - `tabNumber Card`.`document_type` in ({allowed_doctypes}) - '''.format( - allowed_doctypes=','.join(allowed_doctypes) - ) + {doctype_condition} + '''.format(doctype_condition=doctype_condition) def has_permission(doc, ptype, user): roles = frappe.get_roles(user) @@ -124,11 +128,16 @@ def create_number_card(args): doc.insert(ignore_permissions=True) return doc +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters): meta = frappe.get_meta(doctype) searchfields = meta.get_search_fields() search_conditions = [] + if not frappe.db.exists('DocType', doctype): + return + if txt: for field in searchfields: search_conditions.append('`tab{doctype}`.`{field}` like %(txt)s'.format(field=field, doctype=doctype, txt=txt)) @@ -172,7 +181,7 @@ def add_card_to_dashboard(args): dashboard_link = frappe.new_doc('Number Card Link') dashboard_link.card = args.name - if args.set_standard: + if args.set_standard and dashboard.is_standard: card = frappe.get_doc('Number Card', dashboard_link.card) card.is_standard = 1 card.module = dashboard.module diff --git a/frappe/desk/search.py b/frappe/desk/search.py index b4b54b4b6e..798e499bb9 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -10,6 +10,7 @@ from frappe.handler import is_whitelisted from frappe import _ from six import string_types import re +import wrapt UNTRANSLATED_DOCTYPES = ["DocType", "Role"] @@ -206,3 +207,15 @@ def scrub_custom_query(query, key, txt): if '%s' in query: query = query.replace('%s', ((txt or '') + '%')) return query + +@wrapt.decorator +def validate_and_sanitize_search_inputs(fn, instance, args, kwargs): + kwargs.update(dict(zip(fn.__code__.co_varnames, args))) + sanitize_searchfield(kwargs['searchfield']) + kwargs['start'] = cint(kwargs['start']) + kwargs['page_len'] = cint(kwargs['page_len']) + + if kwargs['doctype'] and not frappe.db.exists('DocType', kwargs['doctype']): + return [] + + return fn(**kwargs) \ No newline at end of file diff --git a/frappe/email/__init__.py b/frappe/email/__init__.py index d58b35040e..b05aef7639 100644 --- a/frappe/email/__init__.py +++ b/frappe/email/__init__.py @@ -57,6 +57,8 @@ def relink(name, reference_doctype=None, reference_name=None): communication_type = "Communication" and name = %s""", (reference_doctype, reference_name, name)) +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_communication_doctype(doctype, txt, searchfield, start, page_len, filters): user_perms = frappe.utils.user.UserPermissions(frappe.session.user) user_perms.build_permissions() diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index cf8c6e80c6..29cd890bf1 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -251,7 +251,7 @@ class EmailAccount(Document): email_server = None if frappe.local.flags.in_test: - incoming_mails = test_mails + incoming_mails = test_mails or [] else: email_sync_rule = self.build_email_sync_rule() diff --git a/frappe/model/document.py b/frappe/model/document.py index 69a781d6d1..316c576f55 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -1307,6 +1307,16 @@ class Document(BaseDocument): users = set([assignment.owner for assignment in assignments]) return users + def add_tag(self, tag): + """Add a Tag to this document""" + from frappe.desk.doctype.tag.tag import DocTags + DocTags(self.doctype).add(self.name, tag) + + def get_tags(self): + """Return a list of Tags attached to this document""" + from frappe.desk.doctype.tag.tag import DocTags + return DocTags(self.doctype).get_tags(self.name).split(",")[1:] + def execute_action(doctype, name, action, **kwargs): """Execute an action on a document (called by background worker)""" doc = frappe.get_doc(doctype, name) diff --git a/frappe/patches.txt b/frappe/patches.txt index b207a325cb..75750ab59c 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -296,3 +296,4 @@ frappe.patches.v13_0.update_duration_options frappe.patches.v13_0.replace_old_data_import # 2020-06-24 frappe.patches.v13_0.create_custom_dashboards_cards_and_charts frappe.patches.v13_0.rename_is_custom_field_in_dashboard_chart +frappe.patches.v13_0.generate_theme_files_in_public_folder diff --git a/frappe/patches/v13_0/generate_theme_files_in_public_folder.py b/frappe/patches/v13_0/generate_theme_files_in_public_folder.py new file mode 100644 index 0000000000..c5a64780cd --- /dev/null +++ b/frappe/patches/v13_0/generate_theme_files_in_public_folder.py @@ -0,0 +1,15 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe + + +def execute(): + themes = frappe.db.get_all( + "Website Theme", filters={"theme_url": ("not like", "/files/website_theme/%")} + ) + for theme in themes: + doc = frappe.get_doc("Website Theme", theme.name) + doc.generate_bootstrap_theme() + doc.save() diff --git a/frappe/public/js/frappe/form/controls/multiselect_list.js b/frappe/public/js/frappe/form/controls/multiselect_list.js index cd86bdd767..2a7ee5cb10 100644 --- a/frappe/public/js/frappe/form/controls/multiselect_list.js +++ b/frappe/public/js/frappe/form/controls/multiselect_list.js @@ -3,7 +3,7 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({ let template = `