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 = `
-
`;
+ }
+ return formatted_value;
},
Code: function(value) {
return "
" + (value==null ? "" : $("").text(value).html()) + ""
diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js
index 4babbfb653..d86f46cfca 100644
--- a/frappe/public/js/frappe/form/grid_row.js
+++ b/frappe/public/js/frappe/form/grid_row.js
@@ -393,8 +393,11 @@ export default class GridRow {
// sync get_query
field.get_query = this.grid.get_field(df.fieldname).get_query;
- field.df.onchange = function() {
- me.grid.grid_rows[this.doc.idx-1].refresh_field(this.df.fieldname);
+
+ var field_on_change_function = field.df.onchange;
+ field.df.onchange = function(e) {
+ field_on_change_function && field_on_change_function(e);
+ me.grid.grid_rows[this.doc.idx - 1].refresh_field(this.df.fieldname);
};
field.refresh();
if(field.$input) {
diff --git a/frappe/public/js/frappe/form/script_manager.js b/frappe/public/js/frappe/form/script_manager.js
index d7521b1c59..640c64d4fd 100644
--- a/frappe/public/js/frappe/form/script_manager.js
+++ b/frappe/public/js/frappe/form/script_manager.js
@@ -19,7 +19,7 @@ frappe.ui.form.on = frappe.ui.form.on_change = function(doctype, fieldname, hand
let _handler = (...args) => {
try {
- handler(...args);
+ return handler(...args);
} catch (error) {
console.error(handler);
throw error;
diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js
index 4ef6bb59e3..14c089fe39 100644
--- a/frappe/public/js/frappe/ui/filters/filter.js
+++ b/frappe/public/js/frappe/ui/filters/filter.js
@@ -57,7 +57,7 @@ frappe.ui.Filter = class {
this.conditions.push([key, __(`{0}`, [filter.label])]);
for (let fieldtype of Object.keys(this.invalid_condition_map)) {
if (!filter.valid_for_fieldtypes.includes(fieldtype)) {
- this.invalid_condition_map[fieldtype].push(filter.label);
+ this.invalid_condition_map[fieldtype].push(key);
}
}
}
@@ -243,7 +243,7 @@ frappe.ui.Filter = class {
let args = {};
if (this.filters_config[condition].depends_on) {
const field_name = this.filters_config[condition].depends_on;
- const filter_value = this.base_list.get_filter_value(field_name);
+ const filter_value = this.filter_list.get_filter_value(fieldname);
args[field_name] = filter_value;
}
frappe
diff --git a/frappe/public/js/frappe/ui/filters/filter_list.js b/frappe/public/js/frappe/ui/filters/filter_list.js
index 6d8614b17f..bceed83f0c 100644
--- a/frappe/public/js/frappe/ui/filters/filter_list.js
+++ b/frappe/public/js/frappe/ui/filters/filter_list.js
@@ -195,13 +195,18 @@ frappe.ui.FilterGroup = class {
filter_items: (doctype, fieldname) => {
return !this.filter_exists([doctype, fieldname]);
},
- base_list: this.base_list,
+ filter_list: this.base_list || this,
};
let filter = new frappe.ui.Filter(args);
this.filters.push(filter);
return filter;
}
+ get_filter_value(fieldname) {
+ let filter_obj = this.filters.find(f => f.fieldname == fieldname) || {};
+ return filter_obj.value;
+ }
+
filter_exists(filter_value) {
// filter_value of form: [doctype, fieldname, condition, value]
let exists = false;
diff --git a/frappe/public/js/frappe/utils/dashboard_utils.js b/frappe/public/js/frappe/utils/dashboard_utils.js
index 9dcea5f812..d1653d04c6 100644
--- a/frappe/public/js/frappe/utils/dashboard_utils.js
+++ b/frappe/public/js/frappe/utils/dashboard_utils.js
@@ -212,6 +212,55 @@ frappe.dashboard_utils = {
}
return filters;
+ },
+
+ get_dashboard_link_field() {
+ let field = {
+ label: __('Select Dashboard'),
+ fieldtype: 'Link',
+ fieldname: 'dashboard',
+ options: 'Dashboard',
+ };
+
+ if (!frappe.boot.developer_mode) {
+ field.get_query = () => {
+ return {
+ filters: {
+ is_standard: 0
+ }
+ };
+ };
+ }
+
+ return field;
+ },
+
+ get_add_to_dashboard_dialog(docname, doctype, method) {
+ const field = this.get_dashboard_link_field();
+
+ const dialog = new frappe.ui.Dialog({
+ title: __('Add to Dashboard'),
+ fields: [field],
+ primary_action: (values) => {
+ values.name = docname;
+ values.set_standard = frappe.boot.developer_mode;
+ frappe.xcall(
+ method,
+ {args: values}
+ ).then(()=> {
+ let dashboard_route_html =
+ `
${values.dashboard}`;
+ let message =
+ __(`${doctype} ${values.name} added to Dashboard ` + dashboard_route_html);
+
+ frappe.msgprint(message);
+ });
+
+ dialog.hide();
+ }
+ });
+
+ return dialog;
}
};
\ No newline at end of file
diff --git a/frappe/public/js/frappe/utils/help.js b/frappe/public/js/frappe/utils/help.js
index 336d1ef526..ec6f7c8158 100644
--- a/frappe/public/js/frappe/utils/help.js
+++ b/frappe/public/js/frappe/utils/help.js
@@ -17,7 +17,7 @@ frappe.help.show = function(doctype) {
frappe.help.show_video = function(youtube_id, title) {
if (frappe.utils.is_url(youtube_id)) {
- const expression = '(?:youtube.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu.be/)([^\"&?/s]{11})';
+ const expression = '(?:youtube.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu.be/)([^"&?\\s]{11})';
youtube_id = youtube_id.match(expression)[1];
}
diff --git a/frappe/public/js/frappe/views/breadcrumbs.js b/frappe/public/js/frappe/views/breadcrumbs.js
index 0058310e3f..dbe591f6fd 100644
--- a/frappe/public/js/frappe/views/breadcrumbs.js
+++ b/frappe/public/js/frappe/views/breadcrumbs.js
@@ -6,7 +6,6 @@ frappe.breadcrumbs = {
preferred: {
"File": "",
- "Video": "",
"Dashboard": "Customization",
"Dashboard Chart": "Customization",
"Dashboard Chart Source": "Customization"
diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js
index 53d946f75d..29b21242af 100755
--- a/frappe/public/js/frappe/views/communication.js
+++ b/frappe/public/js/frappe/views/communication.js
@@ -44,26 +44,6 @@ frappe.views.CommunicationComposer = Class.extend({
}
});
- $(document).on("upload_complete", function(event, attachment) {
- if(me.dialog.display) {
- var wrapper = $(me.dialog.fields_dict.select_attachments.wrapper);
-
- // find already checked items
- var checked_items = wrapper.find('[data-file-name]:checked').map(function() {
- return $(this).attr("data-file-name");
- });
-
- // reset attachment list
- me.render_attach();
-
- // check latest added
- checked_items.push(attachment.name);
-
- $.each(checked_items, function(i, filename) {
- wrapper.find('[data-file-name="'+ filename +'"]').prop("checked", true);
- });
- }
- })
this.prepare();
this.dialog.show();
@@ -387,77 +367,86 @@ frappe.views.CommunicationComposer = Class.extend({
folder: 'Home/Attachments',
on_success: attachment => {
this.attachments.push(attachment);
- this.render_attach();
+ this.render_attachment_rows(attachment);
}
};
- if(this.frm) {
+ if (this.frm) {
args = {
doctype: this.frm.doctype,
docname: this.frm.docname,
folder: 'Home/Attachments',
on_success: attachment => {
this.frm.attachments.attachment_uploaded(attachment);
- this.render_attach();
+ this.render_attachment_rows(attachment);
}
- }
+ };
}
- $("
"
- +__("Select Attachments")+"
\
-
\
- "
- +__("Add Attachment")+"
").appendTo(attach.empty())
+ $(`
+
+ ${__("Select Attachments")}
+
+
+
+
+
+ ${__("Add Attachment")}
+
+
+ `).appendTo(attach.empty());
+
attach
.find(".add-more-attachments a")
- .on('click',() => new frappe.ui.FileUploader(args));
- this.render_attach();
+ .on('click', () => new frappe.ui.FileUploader(args));
+ this.render_attachment_rows();
},
- render_attach:function(){
- var fields = this.dialog.fields_dict;
- var attach = $(fields.select_attachments.wrapper).find(".attach-list").empty();
- var files = [];
- if (this.attachments && this.attachments.length) {
- files = files.concat(this.attachments);
- }
- if (cur_frm) {
- files = files.concat(cur_frm.get_files());
- }
+ render_attachment_rows: function(attachment) {
+ const select_attachments = this.dialog.fields_dict.select_attachments;
+ const attachment_rows = $(select_attachments.wrapper).find(".attach-list");
+ if (attachment) {
+ attachment_rows.append(this.get_attachment_row(attachment, true));
+ } else {
+ let files = [];
+ if (this.attachments && this.attachments.length) {
+ files = files.concat(this.attachments);
+ }
+ if (this.frm) {
+ files = files.concat(this.frm.get_files());
+ }
- if(files.length) {
- $.each(files, function(i, f) {
- if (!f.file_name) return;
- f.file_url = frappe.urllib.get_full_url(f.file_url);
-
- $(repl('
'
- + '
', f))
- .appendTo(attach)
- });
- }
- this.select_attachments();
- },
- select_attachments:function(){
- let me = this;
- if(me.dialog.display) {
- let wrapper = $(me.dialog.fields_dict.select_attachments.wrapper);
-
- let unchecked_items = wrapper.find('[data-file-name]:not(:checked)').map(function() {
- return $(this).attr("data-file-name");
- });
-
- $.each(unchecked_items, function(i, filename) {
- wrapper.find('[data-file-name="'+ filename +'"]').prop("checked", true);
- });
+ if (files.length) {
+ $.each(files, (i, f) => {
+ if (!f.file_name) return;
+ if (!attachment_rows.find(`[data-file-name="${f.name}"]`).length) {
+ f.file_url = frappe.urllib.get_full_url(f.file_url);
+ attachment_rows.append(this.get_attachment_row(f));
+ }
+ });
+ }
}
},
+
+ get_attachment_row(attachment, checked) {
+ return $(`
+
+
`);
+ },
+
setup_email: function() {
// email
- var me = this;
var fields = this.dialog.fields_dict;
if(this.attach_document_print) {
diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js
index 4ad39634ef..3db6302188 100644
--- a/frappe/public/js/frappe/views/reports/query_report.js
+++ b/frappe/public/js/frappe/views/reports/query_report.js
@@ -168,6 +168,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
add_card_to_dashboard() {
let field_options = frappe.report_utils.get_field_options_from_report(this.columns, this.raw_data);
+ const dashboard_field = frappe.dashboard_utils.get_dashboard_link_field();
+ const set_standard = frappe.boot.developer_mode;
+
const dialog = new frappe.ui.Dialog({
title: __('Create Card'),
fields: [
@@ -192,12 +195,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
label: __('Add to Dashboard'),
fieldtype: 'Section Break'
},
- {
- fieldname: 'dashboard',
- label: __('Choose Dashboard'),
- fieldtype: 'Link',
- options: 'Dashboard',
- },
+ dashboard_field,
{
fieldname: 'cb_2',
fieldtype: 'Column Break'
@@ -213,7 +211,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
if (!values.label) {
values.label = `${values.report_function} of ${toTitle(values.report_field)}`;
}
- this.create_number_card(values, values.dashboard, values.label);
+ this.create_number_card(values, values.dashboard, values.label, set_standard);
dialog.hide();
}
});
@@ -224,27 +222,26 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
add_chart_to_dashboard() {
if (this.chart_fields || this.chart_options) {
+ const dashboard_field = frappe.dashboard_utils.get_dashboard_link_field();
+ const set_standard = frappe.boot.developer_mode;
+
const dialog = new frappe.ui.Dialog({
title: __('Create Chart'),
fields: [
- {
- fieldname: 'dashboard',
- label: 'Choose Dashboard',
- fieldtype: 'Link',
- options: 'Dashboard',
- },
{
fieldname: 'dashboard_chart_name',
label: 'Chart Name',
fieldtype: 'Data',
- }
+ },
+ dashboard_field,
],
primary_action_label: __('Add'),
primary_action: (values) => {
this.create_dashboard_chart(
this.chart_fields || this.chart_options,
values.dashboard,
- values.dashboard_chart_name
+ values.dashboard_chart_name,
+ set_standard
);
dialog.hide();
}
@@ -256,12 +253,13 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
}
}
- create_number_card(values, dashboard_name, card_name) {
+ create_number_card(values, dashboard_name, card_name, set_standard) {
let args = {
'dashboard': dashboard_name || null,
'type': 'Report',
'report_name': this.report_name,
'filters_json': JSON.stringify(this.get_filter_values()),
+ set_standard: set_standard,
};
Object.assign(args, values);
@@ -274,7 +272,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
);
}
- create_dashboard_chart(chart_args, dashboard_name, chart_name) {
+ create_dashboard_chart(chart_args, dashboard_name, chart_name, set_standard) {
let args = {
'dashboard': dashboard_name || null,
'chart_type': 'Report',
@@ -282,7 +280,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
'type': chart_args.chart_type || frappe.model.unscrub(chart_args.type),
'color': chart_args.color,
'filters_json': JSON.stringify(this.get_filter_values()),
- 'custom_options': {}
+ 'custom_options': {},
+ 'set_standard': set_standard,
};
for (let key in chart_args) {
@@ -303,7 +302,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
'y_axis': chart_args.y_axis_fields.map(f => {
return {'y_field': f.y_field, 'color': f.color};
}),
- 'is_custom': 0
+ 'use_report_chart': 0
}
);
} else {
@@ -311,7 +310,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
Object.assign(args,
{
'chart_name': chart_name,
- 'is_custom': 1
+ 'use_report_chart': 1
}
);
}
@@ -969,8 +968,18 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
if (column.isHeader && !data && this.data) {
// totalRow doesn't have a data object
// proxy it using the first data object
- // this is needed only for currency formatting
- data = this.data[0];
+ // applied to Float, Currency fields, needed only for currency formatting.
+ // make first data column have value 'Total'
+ let index = 1;
+ if (this.datatable && this.datatable.options.checkboxColumn) index = 2;
+
+ if (column.colIndex === index && !value) {
+ value = "Total";
+ column.fieldtype = "Data"; // avoid type issues for value if Date column
+ } else if (in_list(["Currency", "Float"], column.fieldtype)) {
+ // proxy for currency and float
+ data = this.data[0];
+ }
}
return frappe.format(value, column,
{for_print: false, always_show_decimals: true}, data);
diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js
index ab05abb670..0027d7402b 100644
--- a/frappe/public/js/frappe/views/reports/report_view.js
+++ b/frappe/public/js/frappe/views/reports/report_view.js
@@ -767,10 +767,12 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
const index = this.fields.findIndex(f => column.field === f[0]);
if (index === -1) return;
const field = this.fields[index];
- if (field[0] === 'name' && this.group_by === null) {
+
+ if (field[0] === 'name') {
this.refresh();
frappe.throw(__('Cannot remove ID field'));
}
+
this.fields.splice(index, 1);
this.build_fields();
this.setup_columns();
@@ -849,7 +851,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
columns: 2,
options: columns[this.doctype]
.filter(df => {
- return !df.hidden;
+ return !df.hidden && df.fieldname !== 'name';
})
.map(df => ({
label: __(df.label),
diff --git a/frappe/public/js/frappe/widgets/chart_widget.js b/frappe/public/js/frappe/widgets/chart_widget.js
index db0b7b3162..447f0f133d 100644
--- a/frappe/public/js/frappe/widgets/chart_widget.js
+++ b/frappe/public/js/frappe/widgets/chart_widget.js
@@ -266,7 +266,7 @@ export default class ChartWidget extends Widget {
}
get_report_chart_data(result) {
- if (result.chart && this.chart_doc.is_custom) {
+ if (result.chart && this.chart_doc.use_report_chart) {
return result.chart.data;
} else {
let y_fields = [];
@@ -638,7 +638,7 @@ export default class ChartWidget extends Widget {
update_last_synced() {
if (!this.chart_doc.last_synced_on) {
- return
+ return;
}
let last_synced_text = __("Last synced {0}", [
comment_when(this.chart_doc.last_synced_on)
@@ -680,14 +680,15 @@ export default class ChartWidget extends Widget {
}
update_default_date_filters(report_filters, chart_filters) {
- report_filters.map(f => {
- if (['Date', 'DateRange'].includes(f.fieldtype) && f.default) {
- if (f.reqd || chart_filters[f.fieldname]) {
- chart_filters[f.fieldname] = f.default;
+ if (report_filters) {
+ report_filters.map(f => {
+ if (['Date', 'DateRange'].includes(f.fieldtype) && f.default) {
+ if (f.reqd || chart_filters[f.fieldname]) {
+ chart_filters[f.fieldname] = f.default;
+ }
}
- }
- });
-
+ });
+ }
return chart_filters;
}
diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js
index 575926f45c..5ab8044d97 100644
--- a/frappe/public/js/frappe/widgets/number_card_widget.js
+++ b/frappe/public/js/frappe/widgets/number_card_widget.js
@@ -165,7 +165,7 @@ export default class NumberCardWidget extends Widget {
get_number() {
return frappe.xcall(this.settings.method, this.settings.args).then(res => {
- this.settings.get_number(res);
+ return this.settings.get_number(res);
});
}
diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js
index a5916086b0..475568b32c 100644
--- a/frappe/public/js/frappe/widgets/onboarding_widget.js
+++ b/frappe/public/js/frappe/widgets/onboarding_widget.js
@@ -136,7 +136,7 @@ export default class OnboardingWidget extends Widget {
if (step.is_single) {
route = `Form/${step.reference_document}`;
} else {
- route = `Form/${step.reference_document}/${__('New')} ${__(step.reference_document)}`;
+ route = `Form/${step.reference_document}/${__('New')} ${__(step.reference_document)} 1`;
}
let current_route = frappe.get_route();
@@ -262,7 +262,7 @@ export default class OnboardingWidget extends Widget {
frappe.route_hooks.after_save = callback;
}
- frappe.set_route(`Form/${step.reference_document}/${__('New')} ${__(step.reference_document)}`);
+ frappe.set_route(`Form/${step.reference_document}/${__('New')} ${__(step.reference_document)} 1`);
}
show_quick_entry(step) {
diff --git a/frappe/public/js/frappe/widgets/utils.js b/frappe/public/js/frappe/widgets/utils.js
index 233daa7a7d..3cde29ff3d 100644
--- a/frappe/public/js/frappe/widgets/utils.js
+++ b/frappe/public/js/frappe/widgets/utils.js
@@ -131,7 +131,8 @@ function shorten_number(number, country) {
const number_system = get_number_system(country);
let x = Math.abs(Math.round(number));
for (const map of number_system) {
- if (x >= map.divisor) {
+ const condition = map.condition ? map.condition(x) : x >= map.divisor;
+ if (condition) {
return Math.round(number/map.divisor) + ' ' + map.symbol;
}
}
@@ -161,9 +162,14 @@ function get_number_system(country) {
{
divisor: 1.0e+6,
symbol: 'M'
+ },
+ {
+ divisor: 1.0e+3,
+ symbol: 'K',
+ condition: (num) => num.toFixed().length > 5
}]
};
return number_system_map[country];
}
-export { generate_route, generate_grid, build_summary_item, shorten_number };
\ No newline at end of file
+export { generate_route, generate_grid, build_summary_item, shorten_number };
diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js
index 054159116f..2ca26d59e1 100644
--- a/frappe/public/js/frappe/widgets/widget_dialog.js
+++ b/frappe/public/js/frappe/widgets/widget_dialog.js
@@ -174,18 +174,12 @@ class ShortcutDialog extends WidgetDialog {
onchange: () => {
if (this.dialog.get_value("type") == "DocType") {
let doctype = this.dialog.get_value("link_to");
-
- doctype &&
- frappe.db
- .get_value("DocType", doctype, "issingle")
- .then((res) => {
- if (res.message && res.message.issingle) {
- this.hide_filters();
- } else {
- this.setup_filter(doctype);
- this.show_filters();
- }
- });
+ if (doctype && frappe.boot.single_types.includes(doctype)) {
+ this.hide_filters();
+ } else if (doctype) {
+ this.setup_filter(doctype);
+ this.show_filters();
+ }
} else {
this.hide_filters();
}
diff --git a/frappe/public/less/desk.less b/frappe/public/less/desk.less
index 4a45cba94d..70b9ef731b 100644
--- a/frappe/public/less/desk.less
+++ b/frappe/public/less/desk.less
@@ -719,7 +719,6 @@ li.user-progress {
margin-right: 5px;
margin-left: 0px;
position: relative;
- height: 12px;
}
// Will not be required after commonifying lists with empty state
@@ -731,28 +730,11 @@ li.user-progress {
height: 100%;
}
-// mozilla doesn't support
+// Firefox doesn't support
// pseudo elements on checkbox
-@-moz-document url-prefix() {
+@supports (-moz-appearance: none) or (-ms-ime-align:auto) {
input[type="checkbox"] {
- visibility: visible;
- left: 0;
- }
-}
-
-@supports (-moz-appearance: none) {
- input[type="checkbox"] {
- visibility: visible;
- left: 0;
- }
-}
-
-// edge doesn't support pseudo elements on checkbox
-//Microsoft Edge Browser 12+ (All)
-@supports (-ms-ime-align:auto) {
- input[type="checkbox"] {
- visibility: visible;
- left: 0;
+ height: @checkbox-height !important
}
}
diff --git a/frappe/public/less/email.less b/frappe/public/less/email.less
index b6d9540586..bf0507138b 100644
--- a/frappe/public/less/email.less
+++ b/frappe/public/less/email.less
@@ -10,6 +10,13 @@ p {
margin: 1em 0 !important;
}
+.ql-editor {
+ white-space: normal;
+ p {
+ margin: 0 !important;
+ }
+}
+
hr {
border-top: 1px solid @border-color;
}
@@ -210,4 +217,4 @@ hr {
.report-title {
margin-bottom: 20px;
}
-/* csslint ignore:end */
+/* csslint ignore:end */
\ No newline at end of file
diff --git a/frappe/public/less/quill.less b/frappe/public/less/quill.less
index f806dc2f31..ae3b269088 100644
--- a/frappe/public/less/quill.less
+++ b/frappe/public/less/quill.less
@@ -77,7 +77,7 @@
}
}
-.ql-editor .mention {
+.ql-editor:not(.read-mode) .mention {
height: auto;
width: auto;
border-radius: 10px;
@@ -132,3 +132,7 @@
margin-top: 0px;
margin-bottom: 0px;
}
+
+.ql-editor.read-mode {
+ padding: 0;
+}
\ No newline at end of file
diff --git a/frappe/public/scss/website.scss b/frappe/public/scss/website.scss
index edcd25ae9c..c79dfa212b 100644
--- a/frappe/public/scss/website.scss
+++ b/frappe/public/scss/website.scss
@@ -1,5 +1,6 @@
@import "variables";
@import "mixins";
+@import '~quill/dist/quill.core';
@import 'frappe/public/css/font-awesome';
@import "~bootstrap/scss/bootstrap";
@import 'multilevel-dropdown';
@@ -12,6 +13,20 @@
@import 'portal';
@import 'doc';
+.ql-editor.read-mode {
+ padding: 0;
+ line-height: 1.6;
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5 {
+ margin-top: 0.5em;
+ margin-bottom: 0.25em;
+ }
+}
+
.container {
padding-left: 1.25rem;
padding-right: 1.25rem;
@@ -323,4 +338,4 @@ h5.modal-title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
-}
\ No newline at end of file
+}
diff --git a/frappe/templates/base.html b/frappe/templates/base.html
index 0b82b3dac2..8c843a44a4 100644
--- a/frappe/templates/base.html
+++ b/frappe/templates/base.html
@@ -42,7 +42,11 @@
{{ head_include or "" }}
{% endblock -%}
- {%- block style %}{%- endblock -%}
+ {%- block style %}
+ {% if colocated_css -%}
+
+ {%- endif %}
+ {%- endblock -%}
{%- endfor -%}
- {%- block script %}{%- endblock %}
+ {%- block script %}
+ {% if colocated_js -%}
+
+ {%- endif %}
+ {%- endblock %}
{%- block body_include %}{{ body_include or "" }}{% endblock -%}
+
+
Please do not refresh this page...
+
+
+
+
diff --git a/frappe/templates/includes/footer/footer_links.html b/frappe/templates/includes/footer/footer_links.html
index fe9f69fed3..e8bfdadb7f 100644
--- a/frappe/templates/includes/footer/footer_links.html
+++ b/frappe/templates/includes/footer/footer_links.html
@@ -7,8 +7,6 @@
{%- endif -%}
{% endmacro %}
-
-{% if footer_items -%}
-{% endif %}
diff --git a/frappe/templates/includes/image_with_blur.html b/frappe/templates/includes/image_with_blur.html
index dbddb9d8ef..20b0380b88 100644
--- a/frappe/templates/includes/image_with_blur.html
+++ b/frappe/templates/includes/image_with_blur.html
@@ -1,9 +1,7 @@
{%- set res = frappe.utils.get_thumbnail_base64_for_image(src) if src else false -%}
{%- if res and res['base64'].startswith('data:') -%}
+ alt="{{ alt or '' }}" width="{{ res['width'] }}" height="{{ res['height'] }}" data-src="{{ src or '' }}" />
{%- else -%}
{%- endif -%}
diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js
index 8655cdf0fd..dd332b408e 100644
--- a/frappe/templates/includes/login/login.js
+++ b/frappe/templates/includes/login/login.js
@@ -186,7 +186,7 @@ login.login_handlers = (function() {
} else if(data.message == 'Password Reset'){
window.location.href = frappe.utils.sanitise_redirect(data.redirect_to);
} else if(data.message=="No App") {
- login.set_indicator("{{ _("Success") }}", 'green');
+ login.set_indicator("{{ _('Success') }}", 'green');
if(localStorage) {
var last_visited =
localStorage.getItem("last_visited")
diff --git a/frappe/templates/includes/meta_block.html b/frappe/templates/includes/meta_block.html
index 3d867aaef3..1aef8a160e 100644
--- a/frappe/templates/includes/meta_block.html
+++ b/frappe/templates/includes/meta_block.html
@@ -1,5 +1,8 @@
{%- if metatags -%}
{%- for name in metatags %}
-
+{%- set content = metatags.get(name, '') -%}
+{%- if content -%}
+
+{%- endif -%}
{%- endfor -%}
{%- endif -%}
diff --git a/frappe/templates/pages/integrations/paytm_checkout.html b/frappe/templates/pages/integrations/paytm_checkout.html
new file mode 100644
index 0000000000..168f6597e5
--- /dev/null
+++ b/frappe/templates/pages/integrations/paytm_checkout.html
@@ -0,0 +1,43 @@
+{% extends "templates/web.html" %}
+
+{% block title %} Payment {% endblock %}
+
+{%- block header -%}
+
+
+{% endblock %}
+
+{% block script %}
+
+{% endblock %}
+
+{%- block page_content -%}
+
+{% endblock %}
+
+{% block style %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/frappe/templates/pages/integrations/paytm_checkout.py b/frappe/templates/pages/integrations/paytm_checkout.py
new file mode 100644
index 0000000000..bc385b5784
--- /dev/null
+++ b/frappe/templates/pages/integrations/paytm_checkout.py
@@ -0,0 +1,27 @@
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+import json
+from frappe.integrations.doctype.paytm_settings.paytm_settings import get_paytm_params, get_paytm_config
+
+def get_context(context):
+ context.no_cache = 1
+ paytm_config = get_paytm_config()
+
+ try:
+ doc = frappe.get_doc("Integration Request", frappe.form_dict['order_id'])
+
+ context.payment_details = get_paytm_params(json.loads(doc.data), doc.name, paytm_config)
+
+ context.url = paytm_config.url
+
+ except Exception:
+ frappe.log_error()
+ frappe.redirect_to_message(_('Invalid Token'),
+ _('Seems token you are using is invalid!'),
+ http_status_code=400, indicator_color='red')
+
+ frappe.local.flags.redirect_location = frappe.local.response.location
+ raise frappe.Redirect
\ No newline at end of file
diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html
index 07ef577aa7..3681a87f53 100644
--- a/frappe/templates/print_formats/standard_macros.html
+++ b/frappe/templates/print_formats/standard_macros.html
@@ -99,9 +99,7 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}"
{%- if df.fieldtype=="Code" %}
{%- endif -%}
{{ doc.get_formatted(df.fieldname, parent_doc or doc, translated=df.translatable) }}
- {%- if df.fieldtype=="Text Editor" -%}
{%- endif -%}
{% endif -%}
{%- 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) }}
+
+
+
+
+
{{ _("Was this article helpful?") }}
+
+
+
{{ _("Thank you for your feedback!") }}
+
+
+
+
{% 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
{{ _("More articles on {0}").format(category.name) }} @@ -25,4 +39,26 @@
Comments
{% include 'templates/includes/comments/comments.html' %}