diff --git a/.gitignore b/.gitignore index 7df673c1f1..900ae1c7b7 100644 --- a/.gitignore +++ b/.gitignore @@ -188,4 +188,7 @@ typings/ # cypress cypress/screenshots -cypress/videos \ No newline at end of file +cypress/videos + +# JetBrains IDEs +.idea/ diff --git a/frappe/__init__.py b/frappe/__init__.py index d644d2a473..a68c32fe03 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -153,6 +153,7 @@ def init(site, sites_path=None, new_site=False): local.site = site local.sites_path = sites_path local.site_path = os.path.join(sites_path, site) + local.all_apps = None local.request_ip = None local.response = _dict({"docs":[]}) @@ -231,8 +232,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) @@ -300,7 +300,7 @@ def log(msg): debug_log.append(as_unicode(msg)) -def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, alert=False, primary_action=None, is_minimizable=None): +def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, alert=False, primary_action=None, is_minimizable=None, wide=None): """Print a message to the user (via HTTP response). Messages are sent in the `__server_messages` property in the response JSON and shown in a pop-up / modal. @@ -310,6 +310,8 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, :param raise_exception: [optional] Raise given exception and show message. :param as_table: [optional] If `msg` is a list of lists, render as HTML table. :param primary_action: [optional] Bind a primary server/client side action. + :param is_minimizable: [optional] Allow users to minimize the modal + :param wide: [optional] Show wide modal """ from frappe.utils import encode @@ -367,6 +369,9 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, if primary_action: out.primary_action = primary_action + if wide: + out.wide = wide + message_log.append(json.dumps(out)) if raise_exception and hasattr(raise_exception, '__name__'): @@ -388,12 +393,12 @@ def clear_last_message(): if len(local.message_log) > 0: local.message_log = local.message_log[:-1] -def throw(msg, exc=ValidationError, title=None, is_minimizable=None): +def throw(msg, exc=ValidationError, title=None, is_minimizable=None, wide=None): """Throw execption and show message (`msgprint`). :param msg: Message. :param exc: Exception class. Default `frappe.ValidationError`""" - msgprint(msg, raise_exception=exc, title=title, indicator='red', is_minimizable=is_minimizable) + msgprint(msg, raise_exception=exc, title=title, indicator='red', is_minimizable=is_minimizable, wide=wide) def emit_js(js, user=False, **kwargs): if user == False: @@ -436,12 +441,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. @@ -921,10 +922,13 @@ def get_installed_apps(sort=False, frappe_last=False): if not db: connect() + if not local.all_apps: + local.all_apps = get_all_apps(True) + installed = json.loads(db.get_global("installed_apps") or "[]") if sort: - installed = [app for app in get_all_apps(True) if app in installed] + installed = [app for app in local.all_apps if app in installed] if frappe_last: if 'frappe' in installed: @@ -1559,10 +1563,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 +1711,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..39bff83122 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -99,15 +99,16 @@ def application(request): frappe.monitor.stop(response) frappe.recorder.dump() - frappe.logger("frappe.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 hasattr(frappe.local, 'conf') and frappe.local.conf.enable_frappe_logger: + 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"), + "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()) @@ -256,9 +257,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 77468267a4..a20adab569 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, 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/config/integrations.py b/frappe/config/integrations.py index f41adc9ea4..a7ac20065f 100644 --- a/frappe/config/integrations.py +++ b/frappe/config/integrations.py @@ -27,6 +27,11 @@ def get_data(): "name": "Stripe Settings", "description": _("Stripe payment gateway settings"), }, + { + "type": "doctype", + "name": "Paytm Settings", + "description": _("Paytm payment gateway settings"), + }, ] }, { diff --git a/frappe/contacts/address_and_contact.py b/frappe/contacts/address_and_contact.py index 5a004f153b..3ca9547188 100644 --- a/frappe/contacts/address_and_contact.py +++ b/frappe/contacts/address_and_contact.py @@ -146,6 +146,8 @@ def delete_contact_and_address(doctype, docname): if len(doc.links)==1: 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 b85d578353..e82ab9b26e 100644 --- a/frappe/contacts/doctype/address/address.py +++ b/frappe/contacts/doctype/address/address.py @@ -230,6 +230,8 @@ 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 @@ -237,16 +239,17 @@ def address_query(doctype, txt, searchfield, start, page_len, filters): link_name = filters.pop('link_name') condition = "" - for fieldname, value in iteritems(filters): - condition += " and {field}={value}".format( - field=fieldname, - value=value - ) - meta = frappe.get_meta("Address") + for fieldname, value in iteritems(filters): + if meta.get_field(fieldname) or fieldname in frappe.db.DEFAULT_COLUMNS: + condition += " and {field}={value}".format( + field=fieldname, + value=frappe.db.escape(value)) + searchfields = meta.get_search_fields() - if searchfield: + if searchfield and (meta.get_field(searchfield)\ + or searchfield in frappe.db.DEFAULT_COLUMNS): searchfields.append(searchfield) search_condition = '' @@ -289,4 +292,4 @@ def get_condensed_address(doc): return ", ".join([doc.get(d) for d in fields if doc.get(d)]) def update_preferred_address(address, field): - frappe.db.set_value('Address', address, field, 0) \ No newline at end of file + frappe.db.set_value('Address', address, field, 0) diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index 4cf209541c..f82946dc5e 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -182,19 +182,18 @@ def update_contact(doc, method): contact.flags.ignore_mandatory = True 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 + if not frappe.get_meta("Contact").get_field(searchfield)\ + or searchfield not in frappe.db.DEFAULT_COLUMNS: + return [] + link_doctype = filters.pop('link_doctype') link_name = filters.pop('link_name') - condition = "" - for fieldname, value in iteritems(filters): - condition += " and {field}={value}".format( - field=fieldname, - value=value - ) - return frappe.db.sql("""select `tabContact`.name, `tabContact`.first_name, `tabContact`.last_name from @@ -209,9 +208,7 @@ def contact_query(doctype, txt, searchfield, start, page_len, filters): order by if(locate(%(_txt)s, `tabContact`.name), locate(%(_txt)s, `tabContact`.name), 99999), `tabContact`.idx desc, `tabContact`.name - limit %(start)s, %(page_len)s """.format( - mcond=get_match_cond(doctype), - key=searchfield), { + limit %(start)s, %(page_len)s """.format(mcond=get_match_cond(doctype), key=searchfield), { 'txt': '%' + txt + '%', '_txt': txt.replace("%", ""), 'start': start, diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index e9db865ade..4c531fbac6 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -236,7 +236,7 @@ def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None) if doc.sender: # combine for sending to get the format 'Jane ' - doc.sender = formataddr([doc.sender_full_name, doc.sender]) + doc.sender = get_formatted_email(doc.sender_full_name, mail=doc.sender) doc.attachments = [] diff --git a/frappe/core/doctype/data_import/data_import.js b/frappe/core/doctype/data_import/data_import.js index 6a922618cb..0e827a42d8 100644 --- a/frappe/core/doctype/data_import/data_import.js +++ b/frappe/core/doctype/data_import/data_import.js @@ -317,6 +317,7 @@ frappe.ui.form.on('Data Import', { }, show_import_warnings(frm, preview_data) { + let columns = preview_data.columns; let warnings = JSON.parse(frm.doc.template_warnings || '[]'); warnings = warnings.concat(preview_data.warnings || []); @@ -367,11 +368,13 @@ frappe.ui.form.on('Data Import', { .map(warning => { let header = ''; if (warning.col) { - header = __('Column {0}', [warning.col]); + let column_number = `${__('Column {0}', [warning.col])}`; + let column_header = columns[warning.col].header_title; + header = `${column_number} (${column_header})`; } return `
-
${header}
+
${header}
${warning.message}
`; diff --git a/frappe/core/doctype/data_import/data_import_list.js b/frappe/core/doctype/data_import/data_import_list.js index 1dee4319f9..0eb05aa354 100644 --- a/frappe/core/doctype/data_import/data_import_list.js +++ b/frappe/core/doctype/data_import/data_import_list.js @@ -17,6 +17,7 @@ frappe.listview_settings['Data Import'] = { get_indicator: function(doc) { var colors = { 'Pending': 'orange', + 'Not Started': 'orange', 'Partial Success': 'orange', 'Success': 'green', 'In Progress': 'orange', @@ -26,6 +27,9 @@ frappe.listview_settings['Data Import'] = { if (imports_in_progress.includes(doc.name)) { status = 'In Progress'; } + if (status == 'Pending') { + status = 'Not Started'; + } return [__(status), colors[status], 'status,=,' + doc.status]; }, formatters: { diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 910e42af1a..485f7caf08 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -7,7 +7,7 @@ import io import frappe import timeit import json -from datetime import datetime +from datetime import datetime, date from frappe import _ from frappe.utils import cint, flt, update_progress_bar, cstr from frappe.utils.csvutils import read_csv_content, get_csv_content_from_google_sheets @@ -233,7 +233,7 @@ class Importer: return updated_doc else: # throw if no changes - frappe.throw('No changes to update') + frappe.throw("No changes to update") def get_eta(self, current, total, processing_time): self.last_eta = getattr(self, "last_eta", 0) @@ -322,7 +322,7 @@ class ImportFile: if isinstance(file, frappe.string_types): if frappe.db.exists("File", {"file_url": file}): self.file_doc = frappe.get_doc("File", {"file_url": file}) - elif 'docs.google.com/spreadsheets' in file: + elif "docs.google.com/spreadsheets" in file: self.google_sheets_url = file elif os.path.exists(file): self.file_path = file @@ -348,7 +348,7 @@ class ImportFile: elif self.google_sheets_url: content = get_csv_content_from_google_sheets(self.google_sheets_url) - extension = 'csv' + extension = "csv" if not content: frappe.throw(_("Invalid or corrupted content for import")) @@ -602,12 +602,20 @@ class Row: is_table = frappe.get_meta(doctype).istable is_update = self.import_type == UPDATE - if is_table and is_update and doc.get("name") in INVALID_VALUES: - # for table rows being inserted in update - # create a new doc with defaults set - new_doc = frappe.new_doc(doctype, as_dict=True) - new_doc.update(doc) - doc = new_doc + if is_table and is_update: + # check if the row already exists + # if yes, fetch the original doc so that it is not updated + # if no, create a new doc + id_field = get_id_field(doctype) + id_value = doc.get(id_field.fieldname) + if id_value and frappe.db.exists(doctype, id_value): + doc = frappe.get_doc(doctype, id_value) + else: + # for table rows being inserted in update + # create a new doc with defaults set + new_doc = frappe.new_doc(doctype, as_dict=True) + new_doc.update(doc) + doc = new_doc self.check_mandatory_fields(doctype, doc, table_df) return doc @@ -615,16 +623,12 @@ class Row: def validate_value(self, value, col): df = col.df if df.fieldtype == "Select": - select_options = [d for d in (df.options or '').split('\n') if d] + select_options = get_select_options(df) if select_options and value not in select_options: options_string = ", ".join([frappe.bold(d) for d in select_options]) msg = _("Value must be one of {0}").format(options_string) self.warnings.append( - { - "row": self.row_number, - "field": df_as_json(df), - "message": msg, - } + {"row": self.row_number, "field": df_as_json(df), "message": msg,} ) return @@ -635,11 +639,7 @@ class Row: frappe.bold(value), frappe.bold(df.options) ) self.warnings.append( - { - "row": self.row_number, - "field": df_as_json(df), - "message": msg, - } + {"row": self.row_number, "field": df_as_json(df), "message": msg,} ) return elif df.fieldtype in ["Date", "Datetime"]: @@ -668,7 +668,7 @@ class Row: def parse_value(self, value, col): df = col.df - if isinstance(value, datetime) and df.fieldtype in ["Date", "Datetime"]: + if isinstance(value, (datetime, date)) and df.fieldtype in ["Date", "Datetime"]: return value value = cstr(value) @@ -689,7 +689,7 @@ class Row: return value def get_date(self, value, column): - if isinstance(value, datetime): + if isinstance(value, (datetime, date)): return value date_format = column.date_format @@ -786,9 +786,7 @@ class Header(Row): for j, header in enumerate(row): column_values = [get_item_at_index(r, j) for r in raw_data] map_to_field = column_to_field_map.get(str(j)) - column = Column( - j, header, self.doctype, column_values, map_to_field, self.seen - ) + column = Column(j, header, self.doctype, column_values, map_to_field, self.seen) self.seen.append(header) self.columns.append(column) @@ -918,13 +916,20 @@ class Column: self.skip_import = skip_import def guess_date_format_for_column(self): - """ Guesses date format for a column by parsing all the values in the column, + """Guesses date format for a column by parsing all the values in the column, getting the date format and then returning the one which has the maximum frequency """ - date_formats = [ - frappe.utils.guess_date_format(d) for d in self.column_values if isinstance(d, str) - ] + def guess_date_format(d): + if isinstance(d, (datetime, date)): + if self.df.fieldtype == "Date": + return "%Y-%m-%d" + if self.df.fieldtype == "Datetime": + return "%Y-%m-%d %H:%M:%S" + if isinstance(d, str): + return frappe.utils.guess_date_format(d) + + date_formats = [guess_date_format(d) for d in self.column_values] date_formats = [d for d in date_formats if d] if not date_formats: return @@ -955,28 +960,61 @@ class Column: if not self.df: return - if self.df.fieldtype == 'Link': + 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])) - exists = [d.name for d in frappe.db.get_all(self.df.options, filters={'name': ('in', values)})] + exists = [ + d.name for d in frappe.db.get_all(self.df.options, filters={"name": ("in", values)}) + ] not_exists = list(set(values) - set(exists)) if not_exists: - missing_values = ', '.join(not_exists) - self.warnings.append({ - 'col': self.column_number, - 'message': "The following values do not exist for {}: {}".format(self.df.options, missing_values), - 'type': 'warning' - }) + missing_values = ", ".join(not_exists) + self.warnings.append( + { + "col": self.column_number, + "message": ( + "The following values do not exist for {}: {}".format( + self.df.options, missing_values + ) + ), + "type": "warning", + } + ) elif self.df.fieldtype in ("Date", "Time", "Datetime"): # guess date format self.date_format = self.guess_date_format_for_column() if not self.date_format: - self.date_format = '%Y-%m-%d' - self.warnings.append({ - 'col': self.column_number, - 'message': _("Date format could not determined from the values in this column. Defaulting to yyyy-mm-dd."), - 'type': 'info' - }) + self.date_format = "%Y-%m-%d" + self.warnings.append( + { + "col": self.column_number, + "message": _( + "Date format could not be determined from the values in" + " this column. Defaulting to yyyy-mm-dd." + ), + "type": "info", + } + ) + elif self.df.fieldtype == "Select": + options = get_select_options(self.df) + if options: + values = list(set([cstr(v) for v in self.column_values[1:] if v])) + invalid = list(set(values) - set(options)) + if invalid: + valid_values = ", ".join([frappe.bold(o) for o in options]) + invalid_values = ", ".join([frappe.bold(i) for i in invalid]) + self.warnings.append( + { + "col": self.column_number, + "message": ( + "The following values are invalid: {0}. Values must be" + " one of {1}".format(invalid_values, valid_values) + ), + } + ) def as_dict(self): d = frappe._dict() @@ -987,7 +1025,7 @@ class Column: d.map_to_field = self.map_to_field d.date_format = self.date_format d.df = self.df - if hasattr(self.df, 'is_child_table_field'): + if hasattr(self.df, "is_child_table_field"): d.is_child_table_field = self.df.is_child_table_field d.child_table_df = self.df.child_table_df d.skip_import = self.skip_import @@ -1067,7 +1105,7 @@ def build_fields_dict_for_column_matching(parent_doctype): # other fields fields = get_standard_fields(doctype) + frappe.get_meta(doctype).fields for df in fields: - label = (df.label or '').strip() + label = (df.label or "").strip() fieldtype = df.fieldtype or "Data" parent = df.parent or parent_doctype if fieldtype not in no_value_fields: @@ -1161,12 +1199,17 @@ def get_user_format(date_format): .replace("%d", "dd") ) + def df_as_json(df): return { - 'fieldname': df.fieldname, - 'fieldtype': df.fieldtype, - 'label': df.label, - 'options': df.options, - 'parent': df.parent, - 'default': df.default + "fieldname": df.fieldname, + "fieldtype": df.fieldtype, + "label": df.label, + "options": df.options, + "parent": df.parent, + "default": df.default, } + + +def get_select_options(df): + return [d for d in (df.options or "").split("\n") if d] diff --git a/frappe/core/doctype/report/report.js b/frappe/core/doctype/report/report.js index 818c5951e6..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(!frappe.boot.developer_mode && frappe.session.user !== 'Administrator') { + 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/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index 3e6b7a3a98..5c12858e8a 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -52,9 +52,10 @@ class TestServerScript(unittest.TestCase): frappe.db.commit() - # @classmethod - # def tearDownClass(cls): - # frappe.db.sql('truncate `tabServer Script`') + @classmethod + def tearDownClass(cls): + frappe.db.commit() + frappe.db.sql('truncate `tabServer Script`') def setUp(self): frappe.cache().delete_value('server_script_map') diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index e6da49ffbd..825936d8fa 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/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/page/dashboard/dashboard.css b/frappe/core/page/dashboard/dashboard.css deleted file mode 100644 index 8cfaf781b9..0000000000 --- a/frappe/core/page/dashboard/dashboard.css +++ /dev/null @@ -1,16 +0,0 @@ -.dashboard { - margin-top: var(--margin-xs); - padding: 1px; - overflow-x: hidden; -} - -.dashboard .grid-col-2 { - column-gap: 20px; - row-gap: 20px; -} - -/* .restricted-button { - cursor: default; - position: relative; - right: -5px; -} */ \ No newline at end of file diff --git a/frappe/core/page/dashboard/dashboard.js b/frappe/core/page/dashboard/dashboard.js index f17bc1e0b5..7e45163a7e 100644 --- a/frappe/core/page/dashboard/dashboard.js +++ b/frappe/core/page/dashboard/dashboard.js @@ -26,13 +26,6 @@ class Dashboard { `).appendTo(this.wrapper.find(".page-content").empty()); this.container = this.wrapper.find(".dashboard-graph"); this.page = wrapper.page; - - this.page.set_title_sub( - $(``) - ); } show() { @@ -172,19 +165,26 @@ class Dashboard { set_dropdown() { this.page.clear_menu(); - this.page.add_menu_item('Edit...', () => { + this.page.add_menu_item(__('Edit'), () => { frappe.set_route('Form', 'Dashboard', frappe.dashboard.dashboard_name); - }, 1); + }); - this.page.add_menu_item('New...', () => { + this.page.add_menu_item(__('New'), () => { frappe.new_doc('Dashboard'); - }, 1); + }); - frappe.db.get_list("Dashboard").then(dashboards => { + this.page.add_menu_item(__('Refresh All'), () => { + this.chart_group && + this.chart_group.widgets_list.forEach(chart => chart.refresh()); + this.number_card_group && + this.number_card_group.widgets_list.forEach(card => card.render_card()); + }); + + frappe.db.get_list('Dashboard').then(dashboards => { dashboards.map(dashboard => { let name = dashboard.name; if(name != this.dashboard_name){ - this.page.add_menu_item(name, () => frappe.set_route("dashboard", name)); + this.page.add_menu_item(name, () => frappe.set_route("dashboard", name), 1); } }); }); 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 5c75ce697e..cd123c7efa 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/dashboard.js b/frappe/desk/doctype/dashboard/dashboard.js index 237b549433..61300b920b 100644 --- a/frappe/desk/doctype/dashboard/dashboard.js +++ b/frappe/desk/doctype/dashboard/dashboard.js @@ -5,7 +5,7 @@ frappe.ui.form.on('Dashboard', { refresh: function(frm) { frm.add_custom_button(__("Show Dashboard"), () => frappe.set_route('dashboard', frm.doc.name)); - if (!frappe.boot.developer_mode) { + if (!frappe.boot.developer_mode && frm.doc.is_standard) { frm.disable_form(); } diff --git a/frappe/desk/doctype/dashboard/dashboard.json b/frappe/desk/doctype/dashboard/dashboard.json index c7128823fe..95b0846452 100644 --- a/frappe/desk/doctype/dashboard/dashboard.json +++ b/frappe/desk/doctype/dashboard/dashboard.json @@ -54,7 +54,8 @@ "default": "0", "fieldname": "is_standard", "fieldtype": "Check", - "label": "Is Standard" + "label": "Is Standard", + "read_only_depends_on": "eval: !frappe.boot.developer_mode" }, { "depends_on": "eval: doc.is_standard", @@ -66,7 +67,7 @@ } ], "links": [], - "modified": "2020-07-10 17:48:19.468813", + "modified": "2020-07-23 11:05:41.890459", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard", diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js index 738d77ae27..7f26bd9101 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js @@ -23,43 +23,20 @@ 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(); } frm.add_custom_button('Add Chart to Dashboard', () => { - const d = new frappe.ui.Dialog({ - title: __('Add to Dashboard'), - fields: [ - { - label: __('Select Dashboard'), - fieldtype: 'Link', - fieldname: 'dashboard', - options: 'Dashboard', - } - ], - primary_action: (values) => { - values.chart_name = frm.doc.chart_name; - frappe.xcall( - 'frappe.desk.doctype.dashboard_chart.dashboard_chart.add_chart_to_dashboard', - {args: values} - ).then(()=> { - let dashboard_route_html = - `${values.dashboard}`; - let message = - __(`Dashboard Chart ${values.chart_name} add to Dashboard ` + dashboard_route_html); - - frappe.msgprint(message); - }); - - d.hide(); - } - }); + const dialog = frappe.dashboard_utils.get_add_to_dashboard_dialog( + frm.doc.name, + 'Dashboard Chart', + 'frappe.desk.doctype.dashboard_chart.dashboard_chart.add_chart_to_dashboard' + ); if (!frm.doc.chart_name) { frappe.msgprint(__('Please create chart first')); } else { - d.show(); + dialog.show(); } }); @@ -79,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) { @@ -147,6 +119,7 @@ frappe.ui.form.on('Dashboard Chart', { frm.set_df_property('x_field', 'options', []); frm.set_value('filters_json', '{}'); frm.set_value('dynamic_filters_json', '{}'); + frm.set_value('use_report_chart', 0); frm.trigger('set_chart_report_filters'); }, @@ -175,8 +148,8 @@ frappe.ui.form.on('Dashboard Chart', { set_chart_field_options: function(frm) { let filters = frm.doc.filters_json.length > 2 ? JSON.parse(frm.doc.filters_json) : null; - if (frm.doc.dynamic_filters_json.length > 2) { - filters = {...filters, ...JSON.parse(frm.doc.dynamic_filters_json)}; + if (frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2) { + filters = frappe.dashboard_utils.get_all_filters(frm.doc); } frappe.xcall( 'frappe.desk.query_report.run', @@ -187,14 +160,11 @@ frappe.ui.form.on('Dashboard Chart', { } ).then(data => { frm.report_data = data; - if (!data.chart) { - frm.set_value('is_custom', 0); - frm.set_df_property('is_custom', 'hidden', 1); - } else { - frm.set_df_property('is_custom', 'hidden', 0); - } + let report_has_chart = Boolean(data.chart); - if (!frm.doc.is_custom) { + frm.set_df_property('use_report_chart', 'hidden', !report_has_chart); + + if (!frm.doc.use_report_chart) { if (data.result.length) { frm.field_options = frappe.report_utils.get_field_options_from_report(data.columns, data); frm.set_df_property('x_field', 'options', frm.field_options.non_numeric_fields); @@ -315,7 +285,7 @@ frappe.ui.form.on('Dashboard Chart', { set_filters && frm.set_value('filters_json', JSON.stringify(filters)); } - let fields; + let fields = []; if (is_document_type) { fields = [ { @@ -340,7 +310,7 @@ frappe.ui.form.on('Dashboard Chart', { } else if (frm.chart_filters.length) { fields = frm.chart_filters.filter(f => f.fieldname); - fields.map( f => { + fields.map(f => { if (filters[f.fieldname]) { let condition = '='; const filter_row = diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json index d67e725eb9..d4bba53068 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json @@ -12,7 +12,7 @@ "chart_name", "chart_type", "report_name", - "is_custom", + "use_report_chart", "x_field", "y_axis", "source", @@ -194,33 +194,27 @@ "label": "To Date" }, { - "depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.is_custom", + "depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.use_report_chart", "fieldname": "x_field", "fieldtype": "Select", "label": "X Field", - "mandatory_depends_on": "eval: doc.report_name && !doc.is_custom" + "mandatory_depends_on": "eval: doc.report_name && !doc.use_report_chart" }, { "depends_on": "eval:doc.chart_type === 'Report'", "fieldname": "report_name", "fieldtype": "Link", "label": "Report Name", + "mandatory_depends_on": "eval:doc.chart_type === 'Report'", "options": "Report", "set_only_once": 1 }, { - "default": "0", - "depends_on": "eval: doc.report_name", - "fieldname": "is_custom", - "fieldtype": "Check", - "label": "Is Custom" - }, - { - "depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.is_custom", + "depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.use_report_chart", "fieldname": "y_axis", "fieldtype": "Table", "label": "Y Axis", - "mandatory_depends_on": "eval:doc.report_name && !doc.is_custom", + "mandatory_depends_on": "eval:doc.report_name && !doc.use_report_chart", "options": "Dashboard Chart Field" }, { @@ -247,8 +241,7 @@ "fieldname": "is_standard", "fieldtype": "Check", "label": "Is Standard", - "show_days": 1, - "show_seconds": 1 + "read_only_depends_on": "eval: !frappe.boot.developer_mode" }, { "depends_on": "eval: doc.is_standard", @@ -256,28 +249,29 @@ "fieldtype": "Link", "label": "Module", "mandatory_depends_on": "eval: doc.is_standard", - "options": "Module Def", - "show_days": 1, - "show_seconds": 1 + "options": "Module Def" }, { "fieldname": "dynamic_filters_json", "fieldtype": "Code", "label": "Dynamic Filters JSON", - "options": "JSON", - "show_days": 1, - "show_seconds": 1 + "options": "JSON" }, { "fieldname": "dynamic_filters_section", "fieldtype": "Section Break", - "label": "Dynamic Filters", - "show_days": 1, - "show_seconds": 1 + "label": "Dynamic Filters" + }, + { + "default": "0", + "depends_on": "eval: doc.report_name", + "fieldname": "use_report_chart", + "fieldtype": "Check", + "label": "Use Report Chart" } ], "links": [], - "modified": "2020-07-10 16:09:47.102062", + "modified": "2020-07-23 11:10:33.509497", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard Chart", diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 70aece3ee7..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 ) @@ -128,7 +141,13 @@ def add_chart_to_dashboard(args): dashboard = frappe.get_doc('Dashboard', args.dashboard) dashboard_link = frappe.new_doc('Dashboard Chart Link') - dashboard_link.chart = args.chart_name + dashboard_link.chart = args.chart_name or args.name + + 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 + chart.save() dashboard.append('charts', dashboard_link) dashboard.save() @@ -338,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.js b/frappe/desk/doctype/number_card/number_card.js index d5a743818a..63b41b956e 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -32,40 +32,16 @@ frappe.ui.form.on('Number Card', { create_add_to_dashboard_button: function(frm) { frm.add_custom_button('Add Card to Dashboard', () => { - const d = new frappe.ui.Dialog({ - title: __('Add to Dashboard'), - fields: [ - { - label: __('Select Dashboard'), - fieldtype: 'Link', - fieldname: 'dashboard', - options: 'Dashboard', - } - ], - primary_action: (values) => { - values.name = frm.doc.name; - frappe.xcall( - 'frappe.desk.doctype.number_card.number_card.add_card_to_dashboard', - { - args: values - } - ).then(()=> { - let dashboard_route_html = - `${values.dashboard}`; - let message = - __(`Number Card ${values.name} add to Dashboard ` + dashboard_route_html); - - frappe.msgprint(message); - }); - - d.hide(); - } - }); + const dialog = frappe.dashboard_utils.get_add_to_dashboard_dialog( + frm.doc.name, + 'Number Card', + 'frappe.desk.doctype.number_card.number_card.add_card_to_dashboard' + ); if (!frm.doc.name) { frappe.msgprint(__('Please create Card first')); } else { - d.show(); + dialog.show(); } }); }, @@ -140,6 +116,7 @@ frappe.ui.form.on('Number Card', { }, report_name: function(frm) { + frm.filters = []; frm.set_value('filters_json', '{}'); frm.set_value('dynamic_filters_json', '{}'); frm.set_df_property('report_field', 'options', []); @@ -215,8 +192,8 @@ frappe.ui.form.on('Number Card', { set_report_field_options: function(frm) { let filters = frm.doc.filters_json.length > 2 ? JSON.parse(frm.doc.filters_json) : null; - if (frm.doc.dynamic_filters_json.length > 2) { - filters = {...filters, ...JSON.parse(frm.doc.dynamic_filters_json)}; + if (frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2) { + filters = frappe.dashboard_utils.get_all_filters(frm.doc); } frappe.xcall( 'frappe.desk.query_report.run', @@ -271,7 +248,7 @@ frappe.ui.form.on('Number Card', { set_filters && frm.set_value('filters_json', JSON.stringify(filters)); } - let fields; + let fields = []; if (is_document_type) { fields = [ { diff --git a/frappe/desk/doctype/number_card/number_card.json b/frappe/desk/doctype/number_card/number_card.json index e94a06dab8..d3e9598eb7 100644 --- a/frappe/desk/doctype/number_card/number_card.json +++ b/frappe/desk/doctype/number_card/number_card.json @@ -1,7 +1,6 @@ { "actions": [], "allow_rename": 1, - "allow_workflow": 1, "creation": "2020-04-15 18:06:39.444683", "doctype": "DocType", "editable_grid": 1, @@ -116,7 +115,8 @@ "default": "0", "fieldname": "is_standard", "fieldtype": "Check", - "label": "Is Standard" + "label": "Is Standard", + "read_only_depends_on": "eval: !frappe.boot.developer_mode" }, { "depends_on": "eval: doc.is_standard", @@ -191,7 +191,7 @@ } ], "links": [], - "modified": "2020-07-17 18:04:00.814756", + "modified": "2020-07-23 11:11:03.391719", "modified_by": "Administrator", "module": "Desk", "name": "Number Card", diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py index 5b52b60474..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,5 +181,11 @@ def add_card_to_dashboard(args): dashboard_link = frappe.new_doc('Number Card Link') dashboard_link.card = args.name + 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 + card.save() + dashboard.append('cards', dashboard_link) dashboard.save() \ No newline at end of file diff --git a/frappe/desk/doctype/tag/tag.json b/frappe/desk/doctype/tag/tag.json index 895516594e..ad9838d10f 100644 --- a/frappe/desk/doctype/tag/tag.json +++ b/frappe/desk/doctype/tag/tag.json @@ -1,4 +1,5 @@ { + "allow_rename": 1, "autoname": "Prompt", "creation": "2016-05-25 09:43:44.767581", "doctype": "DocType", @@ -46,4 +47,4 @@ ], "sort_field": "modified", "sort_order": "DESC" -} \ No newline at end of file +} diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 0edfd57d4f..d0a32ef076 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -67,8 +67,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None) # Reordered columns columns = json.loads(report.custom_columns) - if report.report_type == 'Query Report': - result = reorder_data_for_custom_columns(columns, query_columns, result) + result = reorder_data_for_custom_columns(columns, query_columns, result, report.report_type) result = add_data_to_custom_columns(columns, result) @@ -216,15 +215,21 @@ def add_data_to_custom_columns(columns, result): return data -def reorder_data_for_custom_columns(custom_columns, columns, result): +def reorder_data_for_custom_columns(custom_columns, columns, result, report_type): + custom_column_labels = [col["label"] for col in custom_columns] + + if report_type == 'Query Report': + original_column_labels = [col.split(":")[0] for col in columns] + else: + original_column_labels = [col["label"] for col in columns] + reordered_result = [] - columns = [col.split(":")[0] for col in columns] for res in result: r = [] - for col in custom_columns: + for col_name in custom_column_labels: try: - idx = columns.index(col.get("label")) + idx = original_column_labels.index(col_name) r.append(res[idx]) except ValueError: pass 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.js b/frappe/email/doctype/email_account/email_account.js index 71f9cccb0d..83896e0af7 100644 --- a/frappe/email/doctype/email_account/email_account.js +++ b/frappe/email/doctype/email_account/email_account.js @@ -95,6 +95,11 @@ frappe.ui.form.on("Email Account", { enable_incoming: function(frm) { frm.doc.no_remaining = null; //perform full sync //frm.set_df_property("append_to", "reqd", frm.doc.enable_incoming); + frm.trigger("warn_autoreply_on_incoming"); + }, + + enable_auto_reply: function(frm) { + frm.trigger("warn_autoreply_on_incoming"); }, notify_if_unreplied: function(frm) { @@ -184,7 +189,18 @@ frappe.ui.form.on("Email Account", { read as well as unread message from server. This may also cause the duplication\ of Communication (emails)."); frappe.confirm(msg, null, function() { - frm.set_value("email_sync_option", "ALL"); + frm.set_value("email_sync_option", "UNSEEN"); + }); + } + }, + + warn_autoreply_on_incoming: function(frm) { + if (frm.doc.enable_incoming && frm.doc.enable_auto_reply && frm.doc.__islocal) { + var msg = __("Enabling auto reply on an incoming email account will send automated replies \ + to all the synchronized emails. Do you wish to continue?"); + frappe.confirm(msg, null, function() { + frm.set_value("enable_auto_reply", 0); + frappe.show_alert({message: __("Disabled Auto Reply"), indicator: "blue"}); }); } } 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/email/doctype/email_account/test_email_account.py b/frappe/email/doctype/email_account/test_email_account.py index 29b54d7f8b..f87ee32bb1 100644 --- a/frappe/email/doctype/email_account/test_email_account.py +++ b/frappe/email/doctype/email_account/test_email_account.py @@ -5,7 +5,10 @@ from __future__ import unicode_literals import frappe, os import unittest, email -test_records = frappe.get_test_records('Email Account') +from frappe.test_runner import make_test_records + +make_test_records("User") +make_test_records("Email Account") from frappe.core.doctype.communication.email import make from frappe.desk.form.load import get_attachments diff --git a/frappe/email/doctype/email_queue/email_queue.json b/frappe/email/doctype/email_queue/email_queue.json index ee8683af22..4529ea8211 100644 --- a/frappe/email/doctype/email_queue/email_queue.json +++ b/frappe/email/doctype/email_queue/email_queue.json @@ -1,640 +1,166 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, + "actions": [], "autoname": "hash", - "beta": 0, "creation": "2012-08-02 15:17:28", - "custom": 0, "description": "Email Queue records.", - "docstatus": 0, "doctype": "DocType", "document_type": "System", - "editable_grid": 0, "engine": "InnoDB", + "field_order": [ + "sender", + "recipients", + "show_as_cc", + "message", + "status", + "error", + "message_id", + "reference_doctype", + "reference_name", + "communication", + "send_after", + "priority", + "add_unsubscribe_link", + "unsubscribe_param", + "unsubscribe_method", + "expose_recipients", + "attachments", + "retry" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "sender", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, + "ignore_xss_filter": 1, "label": "Sender", - "length": 0, - "no_copy": 0, - "options": "Email", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Email" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "recipients", "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Recipient", - "length": 0, - "no_copy": 0, - "options": "Email Queue Recipient", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Email Queue Recipient" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "show_as_cc", "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Show as cc", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Show as cc" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "message", "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Message", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Message" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Not Sent", "fieldname": "status", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 1, "label": "Status", - "length": 0, - "no_copy": 0, - "options": "\nNot Sent\nSending\nSent\nError\nExpired", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "\nNot Sent\nSending\nSent\nError\nExpired" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "error", "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Error", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Error" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "message_id", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Message ID", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "search_index": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "reference_doctype", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Reference Document Type", - "length": 0, - "no_copy": 0, "options": "DocType", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "reference_name", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Reference DocName", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "communication", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Communication", - "length": 0, - "no_copy": 0, "options": "Communication", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "search_index": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "send_after", "fieldtype": "Datetime", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Send After", - "length": 0, "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "priority", "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Priority", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "add_unsubscribe_link", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Add Unsubscribe Link", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Add Unsubscribe Link" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "unsubscribe_param", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Unsubscribe Param", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "unsubscribe_method", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Unsubscribe Method", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Unsubscribe Method" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "expose_recipients", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Expose Recipients", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Expose Recipients" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "attachments", "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Attachments", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "0", "fieldname": "retry", "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Retry", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "read_only": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, "icon": "fa fa-envelope", "idx": 1, - "image_view": 0, "in_create": 1, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-09-05 14:22:27.664645", + "links": [], + "modified": "2020-07-17 15:58:15.369419", "modified_by": "Administrator", "module": "Email", "name": "Email Queue", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 0, "delete": 1, "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "role": "System Manager" } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, + "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index a82b52a663..48688afdb6 100755 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -107,6 +107,9 @@ class Newsletter(WebsiteGenerator): if self.get("__islocal"): throw(_("Please save the Newsletter before sending")) + if not self.recipients: + frappe.throw(_("Newsletter should have at least one recipient")) + def get_context(self, context): newsletters = get_newsletter_list("Newsletter", None, None, 0) if newsletters: diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index 8340d81917..d545190c47 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -11,6 +11,7 @@ import email.utils from six import iteritems, text_type, string_types from email.mime.multipart import MIMEMultipart from email.header import Header +from email import policy def get_email(recipients, sender='', msg='', subject='[No Subject]', @@ -68,8 +69,8 @@ class EMail: self.subject = subject self.expose_recipients = expose_recipients - self.msg_root = MIMEMultipart('mixed') - self.msg_alternative = MIMEMultipart('alternative') + self.msg_root = MIMEMultipart('mixed', policy=policy.SMTPUTF8) + self.msg_alternative = MIMEMultipart('alternative', policy=policy.SMTPUTF8) self.msg_root.attach(self.msg_alternative) self.cc = cc or [] self.bcc = bcc or [] @@ -100,7 +101,7 @@ class EMail: Attach message in the text portion of multipart/alternative """ from email.mime.text import MIMEText - part = MIMEText(message, 'plain', 'utf-8') + part = MIMEText(message, 'plain', 'utf-8', policy=policy.SMTPUTF8) self.msg_alternative.attach(part) def set_part_html(self, message, inline_images): @@ -113,9 +114,9 @@ class EMail: message, _inline_images = replace_filename_with_cid(message) # prepare parts - msg_related = MIMEMultipart('related') + msg_related = MIMEMultipart('related', policy=policy.SMTPUTF8) - html_part = MIMEText(message, 'html', 'utf-8') + html_part = MIMEText(message, 'html', 'utf-8', policy=policy.SMTPUTF8) msg_related.attach(html_part) for image in _inline_images: @@ -124,7 +125,7 @@ class EMail: self.msg_alternative.attach(msg_related) else: - self.msg_alternative.attach(MIMEText(message, 'html', 'utf-8')) + self.msg_alternative.attach(MIMEText(message, 'html', 'utf-8', policy=policy.SMTPUTF8)) def set_html_as_text(self, html): """Set plain text from HTML""" @@ -135,7 +136,7 @@ class EMail: from email.mime.text import MIMEText maintype, subtype = mime_type.split('/') - part = MIMEText(message, _subtype = subtype) + part = MIMEText(message, _subtype = subtype, policy=policy.SMTPUTF8) if as_attachment: part.add_header('Content-Disposition', 'attachment', filename=filename) @@ -222,7 +223,8 @@ class EMail: # reset headers as values may be changed. for key, val in iteritems(headers): - self.set_header(key, val) + if val: + self.set_header(key, val) # call hook to enable apps to modify msg_root before sending for hook in frappe.get_hooks("make_email_body_message"): @@ -238,7 +240,7 @@ class EMail: """validate, build message and convert to string""" self.validate() self.make() - return self.msg_root.as_string() + return self.msg_root.as_string(policy=policy.SMTPUTF8) def get_formatted_html(subject, message, footer=None, print_html=None, email_account=None, header=None, unsubscribe_link=None, sender=None): diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py index 43c4bb8333..705a853bc6 100644 --- a/frappe/email/test_email_body.py +++ b/frappe/email/test_email_body.py @@ -39,7 +39,7 @@ This is the text version of this email subject='Test Subject', content=email_html, text_content=email_text - ).as_string() + ).as_string().replace("\r\n", "\n") def test_prepare_message_returns_already_encoded_string(self): @@ -153,7 +153,7 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> subject='Test Subject', content=email_html, header=['Email Title', 'green'] - ).as_string() + ).as_string().replace("\r\n", "\n") self.assertTrue('''${__('Click here')}`])); + } +}); diff --git a/frappe/integrations/doctype/paytm_settings/paytm_settings.json b/frappe/integrations/doctype/paytm_settings/paytm_settings.json new file mode 100644 index 0000000000..93fbd0df09 --- /dev/null +++ b/frappe/integrations/doctype/paytm_settings/paytm_settings.json @@ -0,0 +1,89 @@ +{ + "actions": [], + "creation": "2020-04-02 00:11:22.846697", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "merchant_id", + "merchant_key", + "staging", + "column_break_4", + "industry_type_id", + "website" + ], + "fields": [ + { + "fieldname": "merchant_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Merchant ID", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "merchant_key", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Merchant Key", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0", + "fieldname": "staging", + "fieldtype": "Check", + "label": "Staging", + "show_days": 1, + "show_seconds": 1 + }, + { + "depends_on": "eval: !doc.staging", + "fieldname": "website", + "fieldtype": "Data", + "label": "Website", + "mandatory_depends_on": "eval: !doc.staging", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "depends_on": "eval: !doc.staging", + "fieldname": "industry_type_id", + "fieldtype": "Data", + "label": "Industry Type ID", + "mandatory_depends_on": "eval: !doc.staging", + "show_days": 1, + "show_seconds": 1 + } + ], + "issingle": 1, + "links": [], + "modified": "2020-06-08 13:36:09.703143", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Paytm Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 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/integrations/doctype/paytm_settings/paytm_settings.py b/frappe/integrations/doctype/paytm_settings/paytm_settings.py new file mode 100644 index 0000000000..616c3837d4 --- /dev/null +++ b/frappe/integrations/doctype/paytm_settings/paytm_settings.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import json +import requests +from six.moves.urllib.parse import urlencode + +import frappe +from frappe.model.document import Document +from frappe import _ +from frappe.utils import get_url, call_hook_method, cint, flt, cstr +from frappe.integrations.utils import create_request_log, create_payment_gateway +from frappe.utils import get_request_site_address +from paytmchecksum import generateSignature, verifySignature +from frappe.utils.password import get_decrypted_password + +class PaytmSettings(Document): + supported_currencies = ["INR"] + + def validate(self): + create_payment_gateway('Paytm') + call_hook_method('payment_gateway_enabled', gateway='Paytm') + + def validate_transaction_currency(self, currency): + if currency not in self.supported_currencies: + frappe.throw(_("Please select another payment method. Paytm does not support transactions in currency '{0}'").format(currency)) + + def get_payment_url(self, **kwargs): + '''Return payment url with several params''' + # create unique order id by making it equal to the integration request + integration_request = create_request_log(kwargs, "Host", "Paytm") + kwargs.update(dict(order_id=integration_request.name)) + + return get_url("./integrations/paytm_checkout?{0}".format(urlencode(kwargs))) + +def get_paytm_config(): + ''' Returns paytm config ''' + + paytm_config = frappe.db.get_singles_dict('Paytm Settings') + paytm_config.update(dict(merchant_key=get_decrypted_password('Paytm Settings', 'Paytm Settings', 'merchant_key'))) + + if cint(paytm_config.staging): + paytm_config.update(dict( + website="WEBSTAGING", + url='https://securegw-stage.paytm.in/order/process', + transaction_status_url='https://securegw-stage.paytm.in/order/status', + industry_type_id='RETAIL' + )) + else: + paytm_config.update(dict( + url='https://securegw.paytm.in/order/process', + transaction_status_url='https://securegw.paytm.in/order/status', + )) + return paytm_config + +def get_paytm_params(payment_details, order_id, paytm_config): + + # initialize a dictionary + paytm_params = dict() + + redirect_uri = get_request_site_address(True) + "/api/method/frappe.integrations.doctype.paytm_settings.paytm_settings.verify_transaction" + + + paytm_params.update({ + "MID" : paytm_config.merchant_id, + "WEBSITE" : paytm_config.website, + "INDUSTRY_TYPE_ID" : paytm_config.industry_type_id, + "CHANNEL_ID" : "WEB", + "ORDER_ID" : order_id, + "CUST_ID" : payment_details['payer_email'], + "EMAIL" : payment_details['payer_email'], + "TXN_AMOUNT" : cstr(flt(payment_details['amount'], 2)), + "CALLBACK_URL" : redirect_uri, + }) + + checksum = generateSignature(paytm_params, paytm_config.merchant_key) + + paytm_params.update({ + "CHECKSUMHASH" : checksum + }) + + return paytm_params + +@frappe.whitelist(allow_guest=True) +def verify_transaction(**paytm_params): + '''Verify checksum for received data in the callback and then verify the transaction''' + paytm_config = get_paytm_config() + is_valid_checksum = False + + paytm_params.pop('cmd', None) + paytm_checksum = paytm_params.pop('CHECKSUMHASH', None) + + if paytm_params and paytm_config and paytm_checksum: + # Verify checksum + is_valid_checksum = verifySignature(paytm_params, paytm_config.merchant_key, paytm_checksum) + + if is_valid_checksum and paytm_params.get('RESPCODE') == '01': + verify_transaction_status(paytm_config, paytm_params['ORDERID']) + else: + frappe.respond_as_web_page("Payment Failed", + "Transaction failed to complete. In case of any deductions, deducted amount will get refunded to your account.", + http_status_code=401, indicator_color='red') + frappe.log_error("Order unsuccessful. Failed Response:"+cstr(paytm_params), 'Paytm Payment Failed') + +def verify_transaction_status(paytm_config, order_id): + '''Verify transaction completion after checksum has been verified''' + paytm_params=dict( + MID=paytm_config.merchant_id, + ORDERID= order_id + ) + + checksum = generateSignature(paytm_params, paytm_config.merchant_key) + paytm_params["CHECKSUMHASH"] = checksum + + post_data = json.dumps(paytm_params) + url = paytm_config.transaction_status_url + + response = requests.post(url, data = post_data, headers = {"Content-type": "application/json"}).json() + finalize_request(order_id, response) + +def finalize_request(order_id, transaction_response): + request = frappe.get_doc('Integration Request', order_id) + transaction_data = frappe._dict(json.loads(request.data)) + redirect_to = transaction_data.get('redirect_to') or None + redirect_message = transaction_data.get('redirect_message') or None + + if transaction_response['STATUS'] == "TXN_SUCCESS": + if transaction_data.reference_doctype and transaction_data.reference_docname: + custom_redirect_to = None + try: + custom_redirect_to = frappe.get_doc(transaction_data.reference_doctype, + transaction_data.reference_docname).run_method("on_payment_authorized", 'Completed') + request.db_set('status', 'Completed') + except Exception: + request.db_set('status', 'Failed') + frappe.log_error(frappe.get_traceback()) + + if custom_redirect_to: + redirect_to = custom_redirect_to + + redirect_url = '/integrations/payment-success' + else: + request.db_set('status', 'Failed') + redirect_url = '/integrations/payment-failed' + + if redirect_to: + redirect_url += '?' + urlencode({'redirect_to': redirect_to}) + if redirect_message: + redirect_url += '&' + urlencode({'redirect_message': redirect_message}) + + frappe.local.response['type'] = 'redirect' + frappe.local.response['location'] = redirect_url + +def get_gateway_controller(doctype, docname): + reference_doc = frappe.get_doc(doctype, docname) + gateway_controller = frappe.db.get_value("Payment Gateway", reference_doc.payment_gateway, "gateway_controller") + return gateway_controller \ No newline at end of file diff --git a/frappe/core/doctype/video/test_video.py b/frappe/integrations/doctype/paytm_settings/test_paytm_settings.py similarity index 80% rename from frappe/core/doctype/video/test_video.py rename to frappe/integrations/doctype/paytm_settings/test_paytm_settings.py index 0bed1e98d6..77a16c82ae 100644 --- a/frappe/core/doctype/video/test_video.py +++ b/frappe/integrations/doctype/paytm_settings/test_paytm_settings.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe import unittest -class TestVideo(unittest.TestCase): +class TestPaytmSettings(unittest.TestCase): pass diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json index 830afbae53..123bb21e88 100755 --- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json +++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json @@ -74,11 +74,11 @@ }, { "default": "us-east-1", - "description": "See https://docs.aws.amazon.com/de_de/general/latest/gr/rande.html#s3_region for details.", + "description": "See https://docs.aws.amazon.com/general/latest/gr/s3.html for details.", "fieldname": "region", "fieldtype": "Select", "label": "Region", - "options": "us-east-1\nus-east-2\nus-west-1\nus-west-2\nap-south-1\nap-southeast-1\nap-southeast-2\nap-northeast-1\nap-northeast-2\nap-northeast-3\nca-central-1\ncn-north-1\ncn-northwest-1\neu-central-1\neu-west-1\neu-west-2\neu-west-3\neu-north-1\nsa-east-1" + "options": "us-east-1\nus-east-2\nus-west-1\nus-west-2\naf-south-1\nap-east-1\nap-south-1\nap-southeast-1\nap-southeast-2\nap-northeast-1\nap-northeast-2\nap-northeast-3\nca-central-1\ncn-north-1\ncn-northwest-1\neu-central-1\neu-west-1\neu-west-2\neu-west-3\neu-south-1\neu-north-1\nme-south-1\nsa-east-1" }, { "fieldname": "endpoint_url", @@ -151,7 +151,7 @@ "hide_toolbar": 1, "issingle": 1, "links": [], - "modified": "2020-04-13 20:57:24.432183", + "modified": "2020-07-27 17:27:21.400000", "modified_by": "Administrator", "module": "Integrations", "name": "S3 Backup Settings", @@ -172,4 +172,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index c7ef7890b4..09d303bec7 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -702,16 +702,13 @@ class BaseDocument(object): df = self.meta.get_field(fieldname) sanitized_value = value - if df and df.get("fieldtype") in ("Data", "Code", "Small Text", "Text") and df.get("options")=="Email": - sanitized_value = sanitize_email(value) + if df and (df.get("ignore_xss_filter") + or (df.get("fieldtype") in ("Data", "Small Text", "Text") and df.get("options")=="Email") + or df.get("fieldtype") in ("Attach", "Attach Image", "Barcode", "Code") - elif df and (df.get("ignore_xss_filter") - or (df.get("fieldtype")=="Code" and df.get("options")!="Email") - or df.get("fieldtype") in ("Attach", "Attach Image", "Barcode") - - # cancelled and submit but not update after submit should be ignored - or self.docstatus==2 - or (self.docstatus==1 and not df.get("allow_on_submit"))): + # cancelled and submit but not update after submit should be ignored + or self.docstatus==2 + or (self.docstatus==1 and not df.get("allow_on_submit"))): continue else: 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/model/workflow.py b/frappe/model/workflow.py index ea563dfc13..32919b3333 100644 --- a/frappe/model/workflow.py +++ b/frappe/model/workflow.py @@ -307,4 +307,4 @@ def set_workflow_state_on_action(doc, workflow_name, action): for state in workflow.states: if state.doc_status == docstatus: doc.set(workflow_state_field, state.state) - return \ No newline at end of file + return diff --git a/frappe/oauth.py b/frappe/oauth.py index 4dc50366be..122c806072 100644 --- a/frappe/oauth.py +++ b/frappe/oauth.py @@ -4,6 +4,7 @@ import pytz from frappe import _ from frappe.auth import LoginManager +from http import cookies from oauthlib.oauth2.rfc6749.tokens import BearerToken from oauthlib.oauth2.rfc6749.grant_types import AuthorizationCodeGrant, ImplicitGrant, ResourceOwnerPasswordCredentialsGrant, ClientCredentialsGrant, RefreshTokenGrant from oauthlib.oauth2 import RequestValidator @@ -130,15 +131,12 @@ class OAuthWebRequestValidator(RequestValidator): oac.scopes = get_url_delimiter().join(request.scopes) oac.redirect_uri_bound_to_authorization_code = request.redirect_uri oac.client = client_id - oac.user = unquote(cookie_dict['user_id']) + oac.user = unquote(cookie_dict['user_id'].value) oac.authorization_code = code['code'] oac.save(ignore_permissions=True) frappe.db.commit() def authenticate_client(self, request, *args, **kwargs): - - cookie_dict = get_cookie_dict_from_headers(request) - #Get ClientID in URL if request.client_id: oc = frappe.get_doc("OAuth Client", request.client_id) @@ -155,7 +153,9 @@ class OAuthWebRequestValidator(RequestValidator): except Exception as e: print("Failed body authentication: Application %s does not exist".format(cid=request.client_id)) - return frappe.session.user == unquote(cookie_dict.get('user_id', "Guest")) + cookie_dict = get_cookie_dict_from_headers(request) + user_id = unquote(cookie_dict['user_id']) if 'user_id' in cookie_dict else "Guest" + return frappe.session.user == user_id def authenticate_client_id(self, client_id, request, *args, **kwargs): cli_id = frappe.db.get_value('OAuth Client', client_id, 'name') @@ -400,13 +400,10 @@ class OAuthWebRequestValidator(RequestValidator): return True def get_cookie_dict_from_headers(r): + cookie = cookies.BaseCookie() if r.headers.get('Cookie'): - cookie = r.headers.get('Cookie') - cookie = cookie.split("; ") - cookie_dict = {k:v for k,v in (x.split('=') for x in cookie)} - return cookie_dict - else: - return {} + cookie.load(r.headers.get('Cookie')) + return cookie def calculate_at_hash(access_token, hash_alg): """Helper method for calculating an access token diff --git a/frappe/patches.txt b/frappe/patches.txt index f8c767f5a3..75750ab59c 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -295,3 +295,5 @@ frappe.patches.v13_0.update_date_filters_in_user_settings 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/patches/v13_0/rename_is_custom_field_in_dashboard_chart.py b/frappe/patches/v13_0/rename_is_custom_field_in_dashboard_chart.py new file mode 100644 index 0000000000..4da0f8164a --- /dev/null +++ b/frappe/patches/v13_0/rename_is_custom_field_in_dashboard_chart.py @@ -0,0 +1,11 @@ +import frappe +from frappe.model.utils.rename_field import rename_field + +def execute(): + if not frappe.db.table_exists('Dashboard Chart'): + return + + frappe.reload_doc('desk', 'doctype', 'dashboard_chart') + + if frappe.db.has_column('Dashboard Chart', 'is_custom'): + rename_field('Dashboard Chart', 'is_custom', 'use_report_chart') \ No newline at end of file diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js index 252c706e51..e6599b2496 100644 --- a/frappe/printing/doctype/print_format/print_format.js +++ b/frappe/printing/doctype/print_format/print_format.js @@ -35,13 +35,20 @@ frappe.ui.form.on("Print Format", { else if (frm.doc.custom_format && !frm.doc.raw_printing) { frm.set_df_property("html", "reqd", 1); } - frm.add_custom_button(__("Make Default"), function () { - frappe.call({ - method: "frappe.printing.doctype.print_format.print_format.make_default", - args: { - name: frm.doc.name - } - }) + frappe.db.get_value('DocType', frm.doc.doc_type, 'default_print_format', (r) => { + if (r.default_print_format != frm.doc.name) { + frm.add_custom_button(__("Set as Default"), function () { + frappe.call({ + method: "frappe.printing.doctype.print_format.print_format.make_default", + args: { + name: frm.doc.name + }, + callback: function() { + frm.refresh(); + } + }); + }); + } }); } }, diff --git a/frappe/public/css/email.css b/frappe/public/css/email.css index 40c6149927..5c398009ff 100644 --- a/frappe/public/css/email.css +++ b/frappe/public/css/email.css @@ -7,6 +7,12 @@ body { p { margin: 1em 0 !important; } +.ql-editor { + white-space: normal; +} +.ql-editor p { + margin: 0 !important; +} hr { border-top: 1px solid #d1d8dd; } diff --git a/frappe/public/js/frappe/data_import/data_exporter.js b/frappe/public/js/frappe/data_import/data_exporter.js index f6af338235..dee4839b34 100644 --- a/frappe/public/js/frappe/data_import/data_exporter.js +++ b/frappe/public/js/frappe/data_import/data_exporter.js @@ -274,23 +274,29 @@ frappe.data_import.DataExporter = class DataExporter { ? this.column_map[child_fieldname] : this.column_map[doctype]; - let is_field_mandatory = df => (df.fieldname === 'name' && !child_fieldname) - || (df.reqd && this.exporting_for == 'Insert New Records'); + let is_field_mandatory = df => { + if (df.reqd && this.exporting_for == 'Insert New Records') { + return true; + } + if (autoname_field && df.fieldname == autoname_field.fieldname) { + return true; + } + if (df.fieldname === 'name') { + return true; + } + return false; + }; return fields .filter(df => { - if (autoname_field && df.fieldname === autoname_field.fieldname) { + if (autoname_field && df.fieldname === 'name') { return false; } return true; }) .map(df => { - let label = __(df.label); - if (autoname_field && df.fieldname === 'name') { - label = label + ` (${__(autoname_field.label)})`; - } return { - label, + label: __(df.label), value: df.fieldname, danger: is_field_mandatory(df), checked: false, diff --git a/frappe/public/js/frappe/data_import/import_preview.js b/frappe/public/js/frappe/data_import/import_preview.js index 4edcb87aeb..6c17cb4351 100644 --- a/frappe/public/js/frappe/data_import/import_preview.js +++ b/frappe/public/js/frappe/data_import/import_preview.js @@ -98,6 +98,9 @@ frappe.data_import.ImportPreview = class ImportPreview { .replace('%y', 'yy') .replace('%m', 'mm') .replace('%d', 'dd') + .replace('%H', 'HH') + .replace('%M', 'mm') + .replace('%S', 'ss') : null; let column_title = ` @@ -354,4 +357,4 @@ function get_fields_as_options(doctype, column_map) { }); }) ); -} \ No newline at end of file +} diff --git a/frappe/public/js/frappe/form/controls/datetime.js b/frappe/public/js/frappe/form/controls/datetime.js index b487be6eca..c99dfe899f 100644 --- a/frappe/public/js/frappe/form/controls/datetime.js +++ b/frappe/public/js/frappe/form/controls/datetime.js @@ -17,7 +17,7 @@ frappe.ui.form.ControlDatetime = frappe.ui.form.ControlDate.extend({ set_description: function() { const { description } = this.df; const { time_zone } = frappe.sys_defaults; - if (!frappe.datetime.is_timezone_same()) { + if (!this.df.hide_timezone && !frappe.datetime.is_timezone_same()) { if (!description) { this.df.description = time_zone; } else if (!description.includes(time_zone)) { diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 603506a056..56f9430238 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -332,6 +332,10 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ let docfield = frappe.meta.get_docfield(doctype, fieldname); let label = docfield ? docfield.label : frappe.model.unscrub(fieldname); + if (docfield && docfield.fieldtype === 'Check') { + filter[3] = filter[3] ? __('Yes'): __('No'); + } + if (filter[3] && Array.isArray(filter[3]) && filter[3].length > 5) { filter[3] = filter[3].slice(0, 5); filter[3].push('...'); diff --git a/frappe/public/js/frappe/form/controls/multiselect_list.js b/frappe/public/js/frappe/form/controls/multiselect_list.js index 866c62277d..b8c652d841 100644 --- a/frappe/public/js/frappe/form/controls/multiselect_list.js +++ b/frappe/public/js/frappe/form/controls/multiselect_list.js @@ -2,8 +2,8 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({ make_input() { let template = ` {%- endif -%} diff --git a/frappe/tests/test_naming.py b/frappe/tests/test_naming.py index 90ab6a6a94..b47fb809ca 100644 --- a/frappe/tests/test_naming.py +++ b/frappe/tests/test_naming.py @@ -88,6 +88,7 @@ class TestNaming(unittest.TestCase): series = 'TEST-' key = 'TEST-' name = 'TEST-00003' + frappe.db.sql("DELETE FROM `tabSeries` WHERE `name`=%s", series) frappe.db.sql("""INSERT INTO `tabSeries` (name, current) values (%s, 3)""", (series,)) revert_series_if_last(key, name) count = frappe.db.sql("""SELECT current from `tabSeries` where name = %s""", series, as_dict=True)[0] diff --git a/frappe/tests/test_scheduler.py b/frappe/tests/test_scheduler.py index e554fd23be..30818e6f17 100644 --- a/frappe/tests/test_scheduler.py +++ b/frappe/tests/test_scheduler.py @@ -49,7 +49,7 @@ class TestScheduler(TestCase): # 2nd job not loaded self.assertFalse(job.enqueue()) - job.delete() + frappe.db.sql('DELETE FROM `tabScheduled Job Log` WHERE `scheduled_job_type`=%s', job.name) def test_is_dormant(self): self.assertTrue(is_dormant(check_time= get_datetime('2100-01-01 00:00:00'))) diff --git a/frappe/tests/test_search.py b/frappe/tests/test_search.py index 5cab827218..192fe9dd25 100644 --- a/frappe/tests/test_search.py +++ b/frappe/tests/test_search.py @@ -50,3 +50,31 @@ class TestSearch(unittest.TestCase): def tearDown(self): frappe.local.lang = 'en' + + def test_validate_and_sanitize_search_inputs(self): + + # should raise error if searchfield is injectable + self.assertRaises(frappe.DataError, + get_data, *('User', 'Random', 'select * from tabSessions) --', '1', '10', dict())) + + # page_len and start should be converted to int + self.assertListEqual(get_data('User', 'Random', 'email', 'name or (select * from tabSessions)', '10', dict()), + ['User', 'Random', 'email', 0, 10, {}]) + self.assertListEqual(get_data('User', 'Random', 'email', page_len='2', start='10', filters=dict()), + ['User', 'Random', 'email', 10, 2, {}]) + + # DocType can be passed as None which should be accepted + self.assertListEqual(get_data(None, 'Random', 'email', '2', '10', dict()), + [None, 'Random', 'email', 2, 10, {}]) + + # return empty string if passed doctype is invalid + self.assertListEqual(get_data("Random DocType", 'Random', 'email', '2', '10', dict()), []) + + # should not fail if function is called via frappe.call with extra arguments + args = ("Random DocType", 'Random', 'email', '2', '10', dict()) + kwargs = {'as_dict': False} + self.assertListEqual(frappe.call('frappe.tests.test_search.get_data', *args, **kwargs), []) + +@frappe.validate_and_sanitize_search_inputs +def get_data(doctype, txt, searchfield, start, page_len, filters): + return [doctype, txt, searchfield, start, page_len, filters] \ No newline at end of file diff --git a/frappe/translate.py b/frappe/translate.py index 2fc2d3f328..ee1f8fa5ff 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -119,7 +119,7 @@ def get_dict(fortype, name=None): messages += frappe.db.sql("select 'Role:', name from tabRole") messages += frappe.db.sql("select 'Module:', name from `tabModule Def`") - message_dict = make_dict_from_messages(messages) + message_dict = make_dict_from_messages(messages, load_user_translation=False) message_dict.update(get_dict_from_hooks(fortype, name)) # remove untranslated message_dict = {k:v for k, v in iteritems(message_dict) if k!=v} @@ -127,6 +127,7 @@ def get_dict(fortype, name=None): cache.hset("translation_assets", frappe.local.lang, translation_assets, shared=True) translation_map = translation_assets[asset_key] + if fortype == "boot": translation_map.update(get_user_translations(frappe.local.lang)) @@ -144,14 +145,17 @@ def get_dict_from_hooks(fortype, name): return translated_dict -def make_dict_from_messages(messages, full_dict=None): +def make_dict_from_messages(messages, full_dict=None, load_user_translation=True): """Returns translated messages as a dict in Language specified in `frappe.local.lang` :param messages: List of untranslated messages """ out = {} if full_dict==None: - full_dict = get_full_dict(frappe.local.lang) + if load_user_translation: + full_dict = get_full_dict(frappe.local.lang) + else: + full_dict = load_lang(frappe.local.lang) for m in messages: if m[1] in full_dict: @@ -189,11 +193,9 @@ def get_full_dict(lang): try: # get user specific transaltion data user_translations = get_user_translations(lang) - except Exception: - user_translations = None - - if user_translations: frappe.local.lang_full_dict.update(user_translations) + except Exception: + pass return frappe.local.lang_full_dict diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index f996b5109e..1da220dc30 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -63,10 +63,11 @@ def get_email_address(user=None): return frappe.db.get_value("User", user, "email") -def get_formatted_email(user): +def get_formatted_email(user, mail=None): """get Email Address of user formatted as: `John Doe `""" fullname = get_fullname(user) - mail = get_email_address(user) + if not mail: + mail = get_email_address(user) return cstr(make_header(decode_header(formataddr((fullname, mail))))) def extract_email_id(email): diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index bb03c85bf9..7eb00ceccd 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -104,11 +104,11 @@ class BackupGenerator: this_file = cstr(this_file) this_file_path = os.path.join(get_backup_path(), this_file) if not is_file_old(this_file_path, older_than): - if "_private_files" in this_file_path: + if "-private-files" in this_file_path: backup_path_private_files = this_file_path - elif "_files" in this_file_path: + elif "-files" in this_file_path: backup_path_files = this_file_path - elif "_database" in this_file_path: + elif "-database" in this_file_path: backup_path_db = this_file_path elif "site_config" in this_file_path: site_config_backup_path = this_file_path @@ -206,6 +206,27 @@ def get_backup(): recipient_list = odb.send_email() frappe.msgprint(_("Download link for your backup will be emailed on the following email address: {0}").format(', '.join(recipient_list))) + +@frappe.whitelist() +def fetch_latest_backups(): + """Fetches paths of the latest backup taken in the last 30 days + Only for: System Managers + + Returns: + dict: relative Backup Paths + """ + frappe.only_for("System Manager") + odb = BackupGenerator(frappe.conf.db_name, frappe.conf.db_name, frappe.conf.db_password, db_host=frappe.db.host, db_type=frappe.conf.db_type, db_port=frappe.conf.db_port) + database, public, private, config = odb.get_recent_backup(older_than=24 * 30) + + return { + "database": database, + "public": public, + "private": private, + "config": config + } + + def scheduled_backup(older_than=6, ignore_files=False, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, force=False, verbose=False): """this function is called from scheduler deletes backups older than 7 days diff --git a/frappe/utils/change_log.py b/frappe/utils/change_log.py index 53c83cfe99..c75b3289db 100644 --- a/frappe/utils/change_log.py +++ b/frappe/utils/change_log.py @@ -118,8 +118,9 @@ def get_versions(): def get_app_branch(app): '''Returns branch of an app''' try: + null_stream = open(os.devnull, 'wb') result = subprocess.check_output('cd ../apps/{0} && git rev-parse --abbrev-ref HEAD'.format(app), - shell=True) + shell=True, stdin=null_stream, stderr=null_stream) result = safe_decode(result) result = result.strip() return result @@ -128,8 +129,9 @@ def get_app_branch(app): def get_app_last_commit_ref(app): try: + null_stream = open(os.devnull, 'wb') result = subprocess.check_output('cd ../apps/{0} && git rev-parse HEAD --short 7'.format(app), - shell=True) + shell=True, stdin=null_stream, stderr=null_stream) result = safe_decode(result) result = result.strip() return result diff --git a/frappe/utils/data.py b/frappe/utils/data.py index d1b409d1fc..bda201f083 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -8,6 +8,7 @@ import frappe from dateutil.parser._parser import ParserError import subprocess import operator +import json import re, datetime, math, time import babel.dates from babel.core import UnknownLocaleError @@ -1236,8 +1237,6 @@ def is_subset(list_a, list_b): def generate_hash(*args, **kwargs): return frappe.generate_hash(*args, **kwargs) - - def guess_date_format(date_string): DATE_FORMATS = [ r"%d-%m-%Y", @@ -1310,3 +1309,9 @@ def guess_date_format(date_string): if date_format and time_format: return (date_format + ' ' + time_format).strip() + +def validate_json_string(string): + try: + json.loads(string) + except (TypeError, ValueError): + raise frappe.ValidationError \ No newline at end of file diff --git a/frappe/utils/logger.py b/frappe/utils/logger.py index 89e3711b0f..364ffa776d 100755 --- a/frappe/utils/logger.py +++ b/frappe/utils/logger.py @@ -11,56 +11,86 @@ from six import text_type # imports - module imports import frappe +from frappe.utils import get_sites 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] +def get_logger(module=None, with_more_info=False, allow_site=True, filter=None, max_size=100_000, file_count=20): + """Application Logger for your given module + + Args: + module (str, optional): Name of your logger and consequently your log file. Defaults to None. + with_more_info (bool, optional): Will log the form dict using the SiteContextFilter. Defaults to False. + allow_site ((str, bool), optional): Pass site name to explicitly log under it's logs. If True and unspecified, guesses which site the logs would be saved under. Defaults to True. + filter (function, optional): Add a filter function for your logger. Defaults to None. + max_size (int, optional): Max file size of each log file in bytes. Defaults to 100_000. + file_count (int, optional): Max count of log files to be retained via Log Rotation. Defaults to 20. + + Returns: + : Returns a Python logger object with Site and Bench level logging capabilities. + """ + + if allow_site is True: + site = getattr(frappe.local, "site", None) + elif allow_site in get_sites(): + site = allow_site + else: + site = False + + logger_name = "{0}-{1}".format(module, site or "all") + + try: + return frappe.loggers[logger_name] + except KeyError: + pass if not module: module = "frappe" with_more_info = True - logfile = module + '.log' - site = getattr(frappe.local, 'site', None) - LOG_FILENAME = os.path.join('..', 'logs', logfile) + logfile = module + ".log" + log_filename = os.path.join("..", "logs", logfile) - logger = logging.getLogger(module) + logger = logging.getLogger(logger_name) 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) + formatter = logging.Formatter("%(asctime)s %(levelname)s {0} %(message)s".format(module)) + handler = RotatingFileHandler(log_filename, maxBytes=max_size, backupCount=file_count) + handler.setFormatter(formatter) logger.addHandler(handler) -# + if site: - SITELOG_FILENAME = os.path.join(site, 'logs', logfile) - site_handler = RotatingFileHandler(SITELOG_FILENAME, maxBytes=100_000, backupCount=20) + sitelog_filename = os.path.join(site, "logs", logfile) + site_handler = RotatingFileHandler(sitelog_filename, maxBytes=max_size, backupCount=file_count) site_handler.setFormatter(formatter) logger.addHandler(site_handler) if with_more_info: handler.addFilter(SiteContextFilter()) - handler.setFormatter(formatter) + if filter: + logger.addFilter(filter) - frappe.loggers[module] = logger + frappe.loggers[logger_name] = logger return logger + class SiteContextFilter(logging.Filter): """This is a filter which injects request information (if available) into the log.""" + def filter(self, record): 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)) + site = getattr(frappe.local, "site", None) + form_dict = getattr(frappe.local, "form_dict", None) + record.msg = text_type(record.msg) + "\nSite: {0}\nForm Dict: {1}".format(site, form_dict) return True + def set_log_level(level): - '''Use this method to set log level to something other than the default DEBUG''' - frappe.log_level = getattr(logging, (level or '').upper(), None) or default_log_level + """Use this method to set log level to something other than the default DEBUG""" + frappe.log_level = getattr(logging, (level or "").upper(), None) or default_log_level frappe.loggers = {} diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index 826aed6082..d97e770d88 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -70,6 +70,7 @@ def get_safe_globals(): render_template=frappe.render_template, msgprint=frappe.msgprint, throw=frappe.throw, + sendmail = frappe.sendmail, user=user, get_fullname=frappe.utils.get_fullname, diff --git a/frappe/website/doctype/help_article/help_article.js b/frappe/website/doctype/help_article/help_article.js index c56bda8e0b..0a87b22e2c 100644 --- a/frappe/website/doctype/help_article/help_article.js +++ b/frappe/website/doctype/help_article/help_article.js @@ -3,6 +3,21 @@ frappe.ui.form.on('Help Article', { refresh: function(frm) { + frm.dashboard.clear_headline(); + frm.dashboard.set_headline_alert(` +
+
+ + Helpful ${frm.doc.helpful} + +
+
+ + Not Helpful ${frm.doc.not_helpful} + +
+
+ `); } }); diff --git a/frappe/website/doctype/help_article/help_article.json b/frappe/website/doctype/help_article/help_article.json index 5957333724..ca659692c6 100644 --- a/frappe/website/doctype/help_article/help_article.json +++ b/frappe/website/doctype/help_article/help_article.json @@ -1,445 +1,158 @@ { - "allow_copy": 0, - "allow_guest_to_view": 1, - "allow_import": 1, - "allow_rename": 0, - "beta": 0, - "creation": "2014-10-30 14:25:53.780105", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 0, + "actions": [], + "allow_guest_to_view": 1, + "allow_import": 1, + "creation": "2014-10-30 14:25:53.780105", + "doctype": "DocType", + "field_order": [ + "title", + "category", + "published", + "column_break_4", + "author", + "level", + "section_break_7", + "content", + "likes", + "route", + "owner", + "feedback", + "helpful", + "cb_00", + "not_helpful" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "title", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Title", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "title", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Title", + "reqd": 1, + "search_index": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "category", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Category", - "length": 0, - "no_copy": 0, - "options": "Help Category", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "category", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Category", + "options": "Help Category", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "published", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Published", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "published", + "fieldtype": "Check", + "label": "Published" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_4", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "user_fullname", - "fieldname": "author", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Author", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "user_fullname", + "fieldname": "author", + "fieldtype": "Data", + "label": "Author" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "level", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Level", - "length": 0, - "no_copy": 0, - "options": "Beginner\nIntermediate\nExpert", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "level", + "fieldtype": "Select", + "label": "Level", + "options": "Beginner\nIntermediate\nExpert" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_7", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_7", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "content", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Content", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "content", + "fieldtype": "Text Editor", + "in_global_search": 1, + "label": "Content", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "likes", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Likes", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "likes", + "fieldtype": "Int", + "label": "Likes", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "route", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Route", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "route", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Route" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "user", - "fieldname": "owner", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Owner", - "length": 0, - "no_copy": 0, - "options": "User", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "default": "user", + "fieldname": "owner", + "fieldtype": "Link", + "label": "Owner", + "options": "User" + }, + { + "collapsible": 1, + "fieldname": "feedback", + "fieldtype": "Section Break", + "label": "Feedback" + }, + { + "default": "0", + "fieldname": "helpful", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Helpful", + "read_only": 1 + }, + { + "fieldname": "cb_00", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "not_helpful", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Not Helpful", + "read_only": 1 } - ], - "has_web_view": 1, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "icon-file-alt", - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_published_field": "published", - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-05-18 17:49:52.912440", - "modified_by": "Administrator", - "module": "Website", - "name": "Help Article", - "name_case": "", - "owner": "Administrator", + ], + "has_web_view": 1, + "icon": "icon-file-alt", + "is_published_field": "published", + "links": [], + "modified": "2020-05-08 10:48:19.997789", + "modified_by": "Administrator", + "module": "Website", + "name": "Help Article", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Knowledge Base Editor", - "set_user_permissions": 0, - "share": 0, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Knowledge Base Editor", "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Knowledge Base Contributor", - "set_user_permissions": 0, - "share": 0, - "submit": 0, + "create": 1, + "read": 1, + "role": "Knowledge Base Contributor", "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Guest", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "read": 1, + "role": "Guest" } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "title", - "track_changes": 1, - "track_seen": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "title", + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/website/doctype/help_article/help_article.py b/frappe/website/doctype/help_article/help_article.py index 6220d0aff5..fa26cfef99 100644 --- a/frappe/website/doctype/help_article/help_article.py +++ b/frappe/website/doctype/help_article/help_article.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe from frappe.website.website_generator import WebsiteGenerator -from frappe.utils import is_markdown, markdown +from frappe.utils import is_markdown, markdown, cint from frappe.website.utils import get_comment_list from frappe import _ @@ -100,3 +100,11 @@ def clear_website_cache(path=None): frappe.cache().delete_value("knowledge_base:category_sidebar") frappe.cache().delete_value("knowledge_base:faq") +@frappe.whitelist(allow_guest=True) +def add_feedback(article, helpful): + field = "helpful" + if helpful == "No": + field = "not_helpful" + + value = cint(frappe.db.get_value("Help Article", article, field)) + frappe.db.set_value("Help Article", article, field, value+1, update_modified=False) \ No newline at end of file diff --git a/frappe/website/doctype/help_article/templates/help_article.html b/frappe/website/doctype/help_article/templates/help_article.html index de28f878a9..1b4bf123e6 100644 --- a/frappe/website/doctype/help_article/templates/help_article.html +++ b/frappe/website/doctype/help_article/templates/help_article.html @@ -10,10 +10,24 @@
By {{ author }} on {{ frappe.format_date(creation) }}
{{ level }} - +
{{ content }}
+ +


+ {{ _("More articles on {0}").format(category.name) }}

+ +
+
+ +
+

{{ _("More articles on {0}").format(category.name) }} @@ -25,4 +39,26 @@
Comments
{% include 'templates/includes/comments/comments.html' %}
+ {% endblock %} diff --git a/frappe/website/doctype/web_form/web_form.json b/frappe/website/doctype/web_form/web_form.json index e6b7632e4d..77786734e7 100644 --- a/frappe/website/doctype/web_form/web_form.json +++ b/frappe/website/doctype/web_form/web_form.json @@ -16,6 +16,7 @@ "route_to_success_link", "allow_edit", "allow_multiple", + "apply_document_permissions", "show_in_grid", "allow_delete", "allow_print", @@ -346,14 +347,20 @@ "fieldname": "custom_css_section", "fieldtype": "Section Break", "label": "Custom CSS" + }, + { + "default": "0", + "fieldname": "apply_document_permissions", + "fieldtype": "Check", + "label": "Apply Document Permissions" } ], "has_web_view": 1, "icon": "icon-edit", "is_published_field": "published", "links": [], - "modified": "2019-12-24 14:15:43.497431", - "modified_by": "faris@erpnext.com", + "modified": "2020-06-30 21:49:18.237443", + "modified_by": "Administrator", "module": "Website", "name": "Web Form", "owner": "Administrator", diff --git a/frappe/website/doctype/web_form/web_form.py b/frappe/website/doctype/web_form/web_form.py index a3867f1396..dd9c3fecfd 100644 --- a/frappe/website/doctype/web_form/web_form.py +++ b/frappe/website/doctype/web_form/web_form.py @@ -130,7 +130,7 @@ def get_context(context): if frappe.session.user == "Guest" and frappe.form_dict.name: frappe.throw(_("You need to be logged in to access this {0}.").format(self.doc_type), frappe.PermissionError) - if frappe.form_dict.name and not has_web_form_permission(self.doc_type, frappe.form_dict.name): + if frappe.form_dict.name and not self.has_web_form_permission(self.doc_type, frappe.form_dict.name): frappe.throw(_("You don't have the permissions to access this document"), frappe.PermissionError) self.reset_field_parent() @@ -343,6 +343,27 @@ def get_context(context): frappe.throw(_('Mandatory Information missing:') + '

' + '
'.join(['{0} ({1})'.format(d.label, d.fieldtype) for d in missing])) + def has_web_form_permission(self, doctype, name, ptype='read'): + if frappe.session.user=="Guest": + return False + + if self.apply_document_permissions: + return frappe.get_doc(doctype, name).has_permission() + + # owner matches + elif frappe.db.get_value(doctype, name, "owner")==frappe.session.user: + return True + + elif frappe.has_website_permission(name, ptype=ptype, doctype=doctype): + return True + + elif check_webform_perm(doctype, name): + return True + + else: + return False + + @frappe.whitelist(allow_guest=True) def accept(web_form, data, docname=None, for_payment=False): @@ -391,7 +412,7 @@ def accept(web_form, data, docname=None, for_payment=False): doc.run_method('validate_payment') if doc.name: - if has_web_form_permission(doc.doctype, doc.name, "write"): + if web_form.has_web_form_permission(doc.doctype, doc.name, "write"): doc.save(ignore_permissions=True) else: # only if permissions are present @@ -478,24 +499,6 @@ def delete_multiple(web_form_name, docnames): raise frappe.PermissionError("You do not have permisssion to delete " + ", ".join(restricted_docnames)) -def has_web_form_permission(doctype, name, ptype='read'): - if frappe.session.user=="Guest": - return False - - # owner matches - elif frappe.db.get_value(doctype, name, "owner")==frappe.session.user: - return True - - elif frappe.has_website_permission(name, ptype=ptype, doctype=doctype): - return True - - elif check_webform_perm(doctype, name): - return True - - else: - return False - - def check_webform_perm(doctype, name): doc = frappe.get_doc(doctype, name) if hasattr(doc, "has_webform_permission"): @@ -532,7 +535,7 @@ def get_form_data(doctype, docname=None, web_form_name=None): if docname: doc = frappe.get_doc(doctype, docname) - if has_web_form_permission(doctype, docname, ptype='read'): + if web_form.has_web_form_permission(doctype, docname, ptype='read'): out.doc = doc else: frappe.throw(_("Not permitted"), frappe.PermissionError) diff --git a/frappe/website/router.py b/frappe/website/router.py index db7e6f322c..263d5b0f07 100644 --- a/frappe/website/router.py +++ b/frappe/website/router.py @@ -283,22 +283,18 @@ def setup_source(page_info): # set the source only if it contains raw content html = source - # load css/js files - js, css = '', '' + # load css/js files + js_path = os.path.join(page_info.basepath, (page_info.basename or 'index') + '.js') + if os.path.exists(js_path) and '{% block script %}' not in html: + with io.open(js_path, 'r', encoding = 'utf-8') as f: + js = f.read() + page_info.colocated_js = js - js_path = os.path.join(page_info.basepath, (page_info.basename or 'index') + '.js') - if os.path.exists(js_path): - if not '{% block script %}' in html: - with io.open(js_path, 'r', encoding = 'utf-8') as f: - js = f.read() - html += '\n{% block script %}\n{% endblock %}' - - css_path = os.path.join(page_info.basepath, (page_info.basename or 'index') + '.css') - if os.path.exists(css_path): - if not '{% block style %}' in html: - with io.open(css_path, 'r', encoding='utf-8') as f: - css = f.read() - html += '\n{% block style %}\n\n{% endblock %}' + css_path = os.path.join(page_info.basepath, (page_info.basename or 'index') + '.css') + if os.path.exists(css_path) and '{% block style %}' not in html: + with io.open(css_path, 'r', encoding='utf-8') as f: + css = f.read() + page_info.colocated_css = css if html: page_info.source = html diff --git a/frappe/workflow/doctype/workflow/test_workflow.py b/frappe/workflow/doctype/workflow/test_workflow.py index 9999df40cb..84adc3a096 100644 --- a/frappe/workflow/doctype/workflow/test_workflow.py +++ b/frappe/workflow/doctype/workflow/test_workflow.py @@ -6,10 +6,14 @@ import frappe import unittest from frappe.utils import random_string from frappe.model.workflow import apply_workflow, WorkflowTransitionError, WorkflowPermissionError, get_common_transition_actions +from frappe.test_runner import make_test_records + +make_test_records("User") class TestWorkflow(unittest.TestCase): def setUp(self): frappe.db.sql('DELETE FROM `tabToDo`') + frappe.db.sql("DELETE FROM `tabHas Role` WHERE `role`='Test Approver'") if not getattr(self, 'workflow', None): self.workflow = create_todo_workflow() frappe.set_user('Administrator') @@ -77,7 +81,7 @@ class TestWorkflow(unittest.TestCase): frappe.set_user('test2@example.com') doc = self.test_default_condition() - workflow_actions = frappe.get_all('Workflow Action', fields=['status']) + workflow_actions = frappe.get_all('Workflow Action', fields=['status', 'reference_name']) self.assertEqual(len(workflow_actions), 1) # test if status of workflow actions are updated on approval @@ -101,6 +105,9 @@ class TestWorkflow(unittest.TestCase): todo.reload() self.assertEqual(todo.docstatus, 1) + self.workflow.states[1].doc_status = 0 + self.workflow.save() + def test_if_workflow_set_on_action(self): self.workflow.states[1].doc_status = 1 self.workflow.save() @@ -110,12 +117,17 @@ class TestWorkflow(unittest.TestCase): self.assertEqual(todo.docstatus, 1) self.assertEqual(todo.workflow_state, 'Approved') + self.workflow.states[1].doc_status = 0 + self.workflow.save() + def create_todo_workflow(): if frappe.db.exists('Workflow', 'Test ToDo'): return frappe.get_doc('Workflow', 'Test ToDo').save(ignore_permissions=True) else: frappe.get_doc(dict(doctype='Role', role_name='Test Approver')).insert(ignore_if_duplicate=True) + frappe.db.commit() + frappe.cache().hdel('roles', frappe.session.user) workflow = frappe.new_doc('Workflow') workflow.workflow_name = 'Test ToDo' workflow.document_type = 'ToDo' @@ -146,4 +158,4 @@ def create_todo_workflow(): return workflow def create_new_todo(): - return frappe.get_doc(dict(doctype='ToDo', description='workflow ' + random_string(10))).insert() \ No newline at end of file + return frappe.get_doc(dict(doctype='ToDo', description='workflow ' + random_string(10))).insert() diff --git a/frappe/www/message.html b/frappe/www/message.html index 87d311d9cc..2919592901 100644 --- a/frappe/www/message.html +++ b/frappe/www/message.html @@ -24,7 +24,7 @@ html, body { {{ title or _("Message") }} -
+
{% block message_body %}

{{ message or "" }}

{% if primary_action %} diff --git a/frappe/www/sitemap.py b/frappe/www/sitemap.py index 8b93270ab5..f8f03c45f8 100644 --- a/frappe/www/sitemap.py +++ b/frappe/www/sitemap.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe.model.document import get_controller -from frappe.utils import get_request_site_address, get_datetime, nowdate +from frappe.utils import get_datetime, nowdate, get_url from frappe.website.router import get_pages, get_all_page_context_from_doctypes from six import iteritems from six.moves.urllib.parse import quote, urljoin @@ -25,13 +25,13 @@ def get_context(context): for route, page in iteritems(get_pages()): if page.sitemap: links.append({ - "loc": urljoin(host, quote(page.name.encode("utf-8"))), + "loc": get_url(quote(page.name.encode("utf-8"))), "lastmod": nowdate() }) for route, data in iteritems(get_public_pages_from_doctypes()): links.append({ - "loc": urljoin(host, quote((route or "").encode("utf-8"))), + "loc": get_url(quote((route or "").encode("utf-8"))), "lastmod": get_datetime(data.get("modified")).strftime("%Y-%m-%d") }) diff --git a/generate_bootstrap_theme.js b/generate_bootstrap_theme.js index bb2a121052..83955856f5 100644 --- a/generate_bootstrap_theme.js +++ b/generate_bootstrap_theme.js @@ -10,7 +10,7 @@ scss_content = scss_content.replace(/\\n/g, '\n'); sass.render({ data: scss_content, - outputStyle: 'compact', + outputStyle: 'compressed', importer: function(url) { if (url.startsWith('~')) { // strip ~ so that it can resolve from node_modules diff --git a/requirements.txt b/requirements.txt index e0ca1a6fad..2995553541 100644 --- a/requirements.txt +++ b/requirements.txt @@ -68,3 +68,6 @@ Werkzeug==0.16.1 Whoosh==2.7.4 xlrd==1.2.0 zxcvbn-python==4.4.24 +pycryptodome==3.9.8 +paytmchecksum==1.7.0 +wrapt==1.10.11 \ No newline at end of file