diff --git a/.eslintrc b/.eslintrc
index 7e469f7672..e79571f556 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -78,6 +78,7 @@
"has_common": true,
"has_words": true,
"validate_email": true,
+ "validate_phone": true,
"get_number_format": true,
"format_number": true,
"format_currency": true,
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 0000000000..2ff8752871
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1,17 @@
+# Each line is a file pattern followed by one or more owners.
+
+# These owners will be the default owners for everything in
+# the repo. Unless a later match takes precedence,
+
+* @surajshetty3416, @netchampfaris
+website/ @scmmishra
+templates/ @scmmishra
+www/ @scmmishra
+integrations/ @Mangesh-Khairnar
+patches/ @surajshetty3416 @sahil28297
+dashboard/ @prssanna
+email/ @Thunderbottom
+event_streaming/ @ruchamahabal
+data_import* @netchampfaris
+core/ @surajshetty3416
+requirements.txt @gavindsouza
\ No newline at end of file
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 08e082c234..f970f51419 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -15,7 +15,7 @@ import frappe
import frappe.website.render
from frappe import _
from frappe.utils import now, cint
-from frappe.model import no_value_fields, default_fields, data_fieldtypes, table_fields
+from frappe.model import no_value_fields, default_fields, data_fieldtypes, table_fields, data_field_options
from frappe.model.document import Document
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
@@ -99,7 +99,6 @@ class DocType(Document):
if self.default_print_format and not self.custom:
frappe.throw(_('Standard DocType cannot have default print format, use Customize Form'))
-
def set_default_in_list_view(self):
'''Set default in-list-view for first 4 mandatory fields'''
if not [d.fieldname for d in self.fields if d.in_list_view]:
@@ -110,14 +109,12 @@ class DocType(Document):
cnt += 1
if cnt == 4: break
-
def set_default_translatable(self):
'''Ensure that non-translatable never will be translatable'''
for d in self.fields:
if d.translatable and not supports_translation(d.fieldtype):
d.translatable = 0
-
def check_developer_mode(self):
"""Throw exception if not developer mode or via patch"""
if frappe.flags.in_patch or frappe.flags.in_test:
@@ -126,7 +123,6 @@ class DocType(Document):
if not frappe.conf.get("developer_mode") and not self.custom:
frappe.throw(_("Not in Developer Mode! Set in site_config.json or make 'Custom' DocType."), CannotCreateStandardDoctypeError)
-
def setup_fields_to_fetch(self):
'''Setup query to update values for newly set fetch values'''
try:
@@ -171,21 +167,18 @@ class DocType(Document):
)
)
-
def update_fields_to_fetch(self):
'''Update fetch values based on queries setup'''
if self.flags.update_fields_to_fetch_queries and not self.issingle:
for query in self.flags.update_fields_to_fetch_queries:
frappe.db.sql(query)
-
def validate_document_type(self):
if self.document_type=="Transaction":
self.document_type = "Document"
if self.document_type=="Master":
self.document_type = "Setup"
-
def validate_website(self):
"""Ensure that website generator has field 'route'"""
if self.has_web_view:
@@ -196,7 +189,6 @@ class DocType(Document):
# clear website cache
frappe.website.render.clear_cache()
-
def change_modified_of_parent(self):
"""Change the timestamp of parent DocType if the current one is a child to clear caches."""
if frappe.flags.in_import:
@@ -206,7 +198,6 @@ class DocType(Document):
for p in parent_list:
frappe.db.sql('UPDATE `tabDocType` SET modified=%s WHERE `name`=%s', (now(), p.parent))
-
def scrub_field_names(self):
"""Sluggify fieldnames if not set from Label."""
restricted = ('name','parent','creation','modified','modified_by',
@@ -236,7 +227,6 @@ class DocType(Document):
# unique is automatically an index
if d.unique: d.search_index = 0
-
def validate_series(self, autoname=None, name=None):
"""Validate if `autoname` property is correctly set."""
if not autoname: autoname = self.autoname
@@ -273,7 +263,6 @@ class DocType(Document):
if used_in:
frappe.throw(_("Series {0} already used in {1}").format(prefix, used_in[0][0]))
-
def on_update(self):
"""Update database schema, make controller templates if `custom` is not set and clear cache."""
self.delete_duplicate_custom_fields()
@@ -327,7 +316,6 @@ class DocType(Document):
dt = {0} and fieldname in ({1})
'''.format('%s', ', '.join(['%s'] * len(fields))), tuple([self.name] + fields), as_dict=True)
-
def sync_global_search(self):
'''If global search settings are changed, rebuild search properties for this table'''
global_search_fields_before_update = [d.fieldname for d in
@@ -345,7 +333,6 @@ class DocType(Document):
frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype',
now=now, doctype=self.name)
-
def set_base_class_for_controller(self):
'''Updates the controller class to subclass from `WebsiteGenertor`,
if it is a subclass of `Document`'''
@@ -365,14 +352,12 @@ class DocType(Document):
with open(controller_path, 'w') as f:
f.write(code)
-
def run_module_method(self, method):
from frappe.modules import load_doctype_module
module = load_doctype_module(self.name, self.module)
if hasattr(module, method):
getattr(module, method)()
-
def before_rename(self, old, new, merge=False):
"""Throw exception if merge. DocTypes cannot be merged."""
if not self.custom and frappe.session.user != "Administrator":
@@ -388,7 +373,6 @@ class DocType(Document):
if not self.custom and not frappe.flags.in_test and not frappe.flags.in_patch:
self.rename_files_and_folders(old, new)
-
def after_rename(self, old, new, merge=False):
"""Change table name using `RENAME TABLE` if table exists. Or update
`doctype` property for Single type."""
@@ -399,7 +383,6 @@ class DocType(Document):
else:
frappe.db.sql("rename table `tab%s` to `tab%s`" % (old, new))
-
def rename_files_and_folders(self, old, new):
# move files
new_path = get_doc_path(self.module, 'doctype', new)
@@ -416,7 +399,6 @@ class DocType(Document):
self.rename_inside_controller(new, old, new_path)
frappe.msgprint(_('Renamed files and replaced code in controllers, please check!'))
-
def rename_inside_controller(self, new, old, new_path):
for fname in ('{}.js', '{}.py', '{}_list.js', '{}_calendar.js', 'test_{}.py', 'test_{}.js'):
fname = os.path.join(new_path, fname.format(frappe.scrub(new)))
@@ -442,7 +424,6 @@ class DocType(Document):
if not (self.issingle and self.istable):
self.preserve_naming_series_options_in_property_setter()
-
def preserve_naming_series_options_in_property_setter(self):
"""Preserve naming_series as property setter if it does not exist"""
naming_series = self.get("fields", {"fieldname": "naming_series"})
@@ -462,7 +443,6 @@ class DocType(Document):
if naming_series[0].default:
make_property_setter(self.name, "naming_series", "default", naming_series[0].default, "Text", validate_fields_for_doctype=False)
-
def before_export(self, docdict):
# remove null and empty fields
def remove_null_fields(o):
@@ -507,7 +487,6 @@ class DocType(Document):
except ValueError:
pass
-
@staticmethod
def prepare_for_import(docdict):
# set order of fields from field_order
@@ -530,19 +509,16 @@ class DocType(Document):
if "field_order" in docdict:
del docdict["field_order"]
-
def export_doc(self):
"""Export to standard folder `[module]/doctype/[name]/[name].json`."""
from frappe.modules.export_file import export_to_files
export_to_files(record_list=[['DocType', self.name]], create_init=True)
-
def import_doc(self):
"""Import from standard folder `[module]/doctype/[name]/[name].json`."""
from frappe.modules.import_module import import_from_files
import_from_files(record_list=[[self.module, 'doctype', self.name]])
-
def make_controller_template(self):
"""Make boilerplate controller template."""
make_boilerplate("controller._py", self)
@@ -559,7 +535,6 @@ class DocType(Document):
make_boilerplate('templates/controller.html', self.as_dict())
make_boilerplate('templates/controller_row.html', self.as_dict())
-
def make_amendable(self):
"""If is_submittable is set, add amended_from docfields."""
if self.is_submittable:
@@ -575,7 +550,6 @@ class DocType(Document):
"no_copy": 1
})
-
def make_repeatable(self):
"""If allow_auto_repeat is set, add auto_repeat custom field."""
if self.allow_auto_repeat:
@@ -644,14 +618,12 @@ class DocType(Document):
})
self.nsm_parent_field = parent_field_name
-
def get_max_idx(self):
"""Returns the highest `idx`"""
max_idx = frappe.db.sql("""select max(idx) from `tabDocField` where parent = %s""",
self.name)
return max_idx and max_idx[0][0] or 0
-
def validate_name(self, name=None):
if not name:
name = self.name
@@ -671,7 +643,6 @@ def validate_fields_for_doctype(doctype):
doc.delete_duplicate_custom_fields()
validate_fields(frappe.get_meta(doctype, cached=False))
-
# this is separate because it is also called via custom field
def validate_fields(meta):
"""Validate doctype fields. Checks
@@ -695,29 +666,24 @@ def validate_fields(meta):
def check_illegal_characters(fieldname):
validate_column_name(fieldname)
-
def check_invalid_fieldnames(docname, fieldname):
invalid_fields = ('doctype',)
if fieldname in invalid_fields:
frappe.throw(_("{0}: Fieldname cannot be one of {1}")
.format(docname, ", ".join([frappe.bold(d) for d in invalid_fields])))
-
def check_unique_fieldname(docname, fieldname):
duplicates = list(filter(None, map(lambda df: df.fieldname==fieldname and str(df.idx) or None, fields)))
if len(duplicates) > 1:
frappe.throw(_("{0}: Fieldname {1} appears multiple times in rows {2}").format(docname, fieldname, ", ".join(duplicates)), UniqueFieldnameError)
-
def check_fieldname_length(fieldname):
validate_column_length(fieldname)
-
def check_illegal_mandatory(docname, d):
if (d.fieldtype in no_value_fields) and d.fieldtype not in table_fields and d.reqd:
frappe.throw(_("{0}: Field {1} of type {2} cannot be mandatory").format(docname, d.label, d.fieldtype), IllegalMandatoryError)
-
def check_link_table_options(docname, d):
if frappe.flags.in_patch: return
if d.fieldtype in ("Link",) + table_fields:
@@ -736,28 +702,23 @@ def validate_fields(meta):
# fix case
d.options = options
-
def check_hidden_and_mandatory(docname, d):
if d.hidden and d.reqd and not d.default:
frappe.throw(_("{0}: Field {1} in row {2} cannot be hidden and mandatory without default").format(docname, d.label, d.idx), HiddenAndMandatoryWithoutDefaultError)
-
def check_width(d):
if d.fieldtype == "Currency" and cint(d.width) < 100:
frappe.throw(_("Max width for type Currency is 100px in row {0}").format(d.idx))
-
def check_in_list_view(d):
if d.in_list_view and (d.fieldtype in not_allowed_in_list_view):
frappe.throw(_("'In List View' not allowed for type {0} in row {1}").format(d.fieldtype, d.idx))
-
def check_in_global_search(d):
if d.in_global_search and d.fieldtype in no_value_fields:
frappe.throw(_("'In Global Search' not allowed for type {0} in row {1}")
.format(d.fieldtype, d.idx))
-
def check_dynamic_link_options(d):
if d.fieldtype=="Dynamic Link":
doctype_pointer = list(filter(lambda df: df.fieldname==d.options, fields))
@@ -765,7 +726,6 @@ def validate_fields(meta):
or (doctype_pointer[0].fieldtype=="Link" and doctype_pointer[0].options!="DocType"):
frappe.throw(_("Options 'Dynamic Link' type of field must point to another Link Field with options as 'DocType'"))
-
def check_illegal_default(d):
if d.fieldtype == "Check" and not d.default:
d.default = '0'
@@ -774,12 +734,10 @@ def validate_fields(meta):
if d.fieldtype == "Select" and d.default and (d.default not in d.options.split("\n")):
frappe.throw(_("Default for {0} must be an option").format(d.fieldname))
-
def check_precision(d):
if d.fieldtype in ("Currency", "Float", "Percent") and d.precision is not None and not (1 <= cint(d.precision) <= 6):
frappe.throw(_("Precision should be between 1 and 6"))
-
def check_unique_and_text(docname, d):
if meta.issingle:
d.unique = 0
@@ -801,7 +759,6 @@ def validate_fields(meta):
if d.search_index and d.fieldtype in ("Text", "Long Text", "Small Text", "Code", "Text Editor"):
frappe.throw(_("{0}:Fieldtype {1} for {2} cannot be indexed").format(docname, d.fieldtype, d.label), CannotIndexedError)
-
def check_fold(fields):
fold_exists = False
for i, f in enumerate(fields):
@@ -816,7 +773,6 @@ def validate_fields(meta):
else:
frappe.throw(_("Fold can not be at the end of the form"))
-
def check_search_fields(meta, fields):
"""Throw exception if `search_fields` don't contain valid fields."""
if not meta.search_fields:
@@ -833,7 +789,6 @@ def validate_fields(meta):
(fieldname not in fieldname_list):
frappe.throw(_("Search field {0} is not valid").format(fieldname))
-
def check_title_field(meta):
"""Throw exception if `title_field` isn't a valid fieldname."""
if not meta.get("title_field"):
@@ -860,7 +815,6 @@ def validate_fields(meta):
_validate_title_field_pattern(df.options)
_validate_title_field_pattern(df.default)
-
def check_image_field(meta):
'''check image_field exists and is of type "Attach Image"'''
if not meta.image_field:
@@ -872,7 +826,6 @@ def validate_fields(meta):
if df[0].fieldtype != 'Attach Image':
frappe.throw(_("Image field must be of type Attach Image"), InvalidFieldNameError)
-
def check_is_published_field(meta):
if not meta.is_published_field:
return
@@ -880,7 +833,6 @@ def validate_fields(meta):
if meta.is_published_field not in fieldname_list:
frappe.throw(_("Is Published Field must be a valid fieldname"), InvalidFieldNameError)
-
def check_timeline_field(meta):
if not meta.timeline_field:
return
@@ -892,7 +844,6 @@ def validate_fields(meta):
if df.fieldtype not in ("Link", "Dynamic Link"):
frappe.throw(_("Timeline field must be a Link or Dynamic Link"), InvalidFieldNameError)
-
def check_sort_field(meta):
'''Validate that sort_field(s) is a valid field'''
if meta.sort_field:
@@ -905,7 +856,6 @@ def validate_fields(meta):
frappe.throw(_("Sort field {0} must be a valid fieldname").format(fieldname),
InvalidFieldNameError)
-
def check_illegal_depends_on_conditions(docfield):
''' assignment operation should not be allowed in the depends on condition.'''
depends_on_fields = ["depends_on", "collapsible_depends_on", "mandatory_depends_on", "read_only_depends_on"]
@@ -915,7 +865,6 @@ def validate_fields(meta):
re.match("""[\w\.:_]+\s*={1}\s*[\w\.@'"]+""", depends_on):
frappe.throw(_("Invalid {0} condition").format(frappe.unscrub(field)), frappe.ValidationError)
-
def check_table_multiselect_option(docfield):
'''check if the doctype provided in Option has atleast 1 Link field'''
if not docfield.fieldtype == 'Table MultiSelect': return
@@ -928,7 +877,6 @@ def validate_fields(meta):
frappe.throw(_('DocType {0} provided for the field {1} must have atleast one Link field')
.format(doctype, docfield.fieldname), frappe.ValidationError)
-
def scrub_options_in_select(field):
"""Strip options for whitespaces"""
@@ -940,11 +888,20 @@ def validate_fields(meta):
options_list.append(_option)
field.options = '\n'.join(options_list)
-
def scrub_fetch_from(field):
if hasattr(field, 'fetch_from') and getattr(field, 'fetch_from'):
field.fetch_from = field.fetch_from.strip('\n').strip()
+ def validate_data_field_type(docfield):
+ if docfield.fieldtype == "Data":
+ if docfield.options and (docfield.options not in data_field_options):
+ df_str = frappe.bold(_(docfield.label))
+ text_str = _("{0} is an invalid Data field.").format(df_str) + "
" * 2 + _("Only Options allowed for Data field are:") + "
"
+ df_options_str = "
- " + "
- ".join([_(x) for x in data_field_options]) + "
"
+
+ frappe.msgprint(text_str + df_options_str, title="Invalid Data Field", raise_exception=True)
+
+
fields = meta.get("fields")
fieldname_list = [d.fieldname for d in fields]
@@ -975,6 +932,7 @@ def validate_fields(meta):
check_table_multiselect_option(d)
scrub_options_in_select(d)
scrub_fetch_from(d)
+ validate_data_field_type(d)
check_fold(fields)
check_search_fields(meta, fields)
@@ -984,7 +942,6 @@ def validate_fields(meta):
check_sort_field(meta)
check_image_field(meta)
-
def validate_permissions_for_doctype(doctype, for_remove=False):
"""Validates if permissions are set correctly."""
doctype = frappe.get_doc("DocType", doctype)
@@ -996,7 +953,6 @@ def validate_permissions_for_doctype(doctype, for_remove=False):
clear_permissions_cache(doctype.name)
-
def clear_permissions_cache(doctype):
frappe.clear_cache(doctype=doctype)
delete_notification_count_for(doctype)
@@ -1011,7 +967,6 @@ def clear_permissions_cache(doctype):
""", doctype):
frappe.clear_cache(user=user)
-
def validate_permissions(doctype, for_remove=False):
permissions = doctype.get("permissions")
if not permissions:
@@ -1105,7 +1060,6 @@ def validate_permissions(doctype, for_remove=False):
check_level_zero_is_set(d)
remove_rights_for_single(d)
-
def make_module_and_roles(doc, perm_fieldname="permissions"):
"""Make `Module Def` and `Role` records if already not made. Called while installing."""
try:
@@ -1136,7 +1090,6 @@ def make_module_and_roles(doc, perm_fieldname="permissions"):
else:
raise
-
def check_if_fieldname_conflicts_with_methods(doctype, fieldname):
doc = frappe.get_doc({"doctype": doctype})
method_list = [method for method in dir(doc) if isinstance(method, str) and callable(getattr(doc, method))]
@@ -1144,7 +1097,6 @@ def check_if_fieldname_conflicts_with_methods(doctype, fieldname):
if fieldname in method_list:
frappe.throw(_("Fieldname {0} conflicting with meta object").format(fieldname))
-
def clear_linked_doctype_cache():
frappe.cache().delete_value('linked_doctypes_without_ignore_user_permissions_enabled')
diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py
index 969a71ab7d..fe9f88b9b3 100644
--- a/frappe/core/doctype/doctype/test_doctype.py
+++ b/frappe/core/doctype/doctype/test_doctype.py
@@ -113,6 +113,32 @@ class TestDocType(unittest.TestCase):
if condition:
self.assertFalse(re.match(pattern, condition))
+ def test_data_field_options(self):
+ doctype_name = "Test Data Fields"
+ valid_data_field_options = frappe.model.data_field_options + ("",)
+ invalid_data_field_options = ("Invalid Option 1", frappe.utils.random_string(5))
+
+ for field_option in (valid_data_field_options + invalid_data_field_options):
+ test_doctype = frappe.get_doc({
+ "doctype": "DocType",
+ "name": doctype_name,
+ "module": "Core",
+ "custom": 1,
+ "fields": [{
+ "fieldname": "{0}_field".format(field_option),
+ "fieldtype": "Data",
+ "options": field_option
+ }]
+ })
+
+ if field_option in invalid_data_field_options:
+ # assert that only data options in frappe.model.data_field_options are valid
+ self.assertRaises(frappe.ValidationError, test_doctype.insert)
+ else:
+ test_doctype.insert()
+ self.assertEqual(test_doctype.name, doctype_name)
+ test_doctype.delete()
+
def test_sync_field_order(self):
from frappe.modules.import_file import get_file_path
import os
@@ -349,4 +375,4 @@ class TestDocType(unittest.TestCase):
# delete doctype
link_doc.delete()
doc.delete()
- frappe.db.commit()
\ No newline at end of file
+ frappe.db.commit()
diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py
index 2d49915f59..967b28b8b2 100644
--- a/frappe/core/doctype/report/report.py
+++ b/frappe/core/doctype/report/report.py
@@ -6,7 +6,7 @@ import frappe
import json, datetime
from frappe import _, scrub
import frappe.desk.query_report
-from frappe.utils import cint
+from frappe.utils import cint, cstr
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
from frappe.modules import make_boilerplate
@@ -92,6 +92,18 @@ class Report(Document):
make_boilerplate("controller.py", self, {"name": self.name})
make_boilerplate("controller.js", self, {"name": self.name})
+ def execute_query_report(self, filters):
+ if not self.query:
+ frappe.throw(_("Must specify a Query to run"), title=_('Report Document Error'))
+
+ if not self.query.lower().startswith("select"):
+ frappe.throw(_("Query must be a SELECT"), title=_('Report Document Error'))
+
+ result = [list(t) for t in frappe.db.sql(self.query, filters)]
+ columns = [cstr(c[0]) for c in frappe.db.get_description()]
+
+ return [columns, result]
+
def execute_script_report(self, filters):
# save the timestamp to automatically set to prepared
threshold = 30
diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js
index c4873ee40e..b17548d994 100644
--- a/frappe/core/doctype/user/user.js
+++ b/frappe/core/doctype/user/user.js
@@ -97,6 +97,48 @@ frappe.ui.form.on('User', {
});
}, __("Password"));
+ frappe.db.get_single_value("LDAP Settings", "enabled").then((value) => {
+ if (value === 1 && frm.doc.name != "Administrator") {
+ frm.add_custom_button(__("Reset LDAP Password"), function() {
+ const d = new frappe.ui.Dialog({
+ title: __("Reset LDAP Password"),
+ fields: [
+ {
+ label: __("New Password"),
+ fieldtype: "Password",
+ fieldname: "new_password",
+ reqd: 1
+ },
+ {
+ label: __("Confirm New Password"),
+ fieldtype: "Password",
+ fieldname: "confirm_password",
+ reqd: 1
+ },
+ {
+ label: __("Logout All Sessions"),
+ fieldtype: "Check",
+ fieldname: "logout_sessions"
+ }
+ ],
+ primary_action: (values) => {
+ d.hide();
+ if (values.new_password !== values.confirm_password) {
+ frappe.throw(__("Passwords do not match!"));
+ }
+ frappe.call(
+ "frappe.integrations.doctype.ldap_settings.ldap_settings.reset_password", {
+ user: frm.doc.email,
+ password: values.new_password,
+ logout: values.logout_sessions
+ });
+ }
+ });
+ d.show();
+ }, __("Password"));
+ }
+ });
+
frm.add_custom_button(__("Reset OTP Secret"), function() {
frappe.call({
method: "frappe.core.doctype.user.user.reset_otp_secret",
diff --git a/frappe/core/page/dashboard/dashboard.js b/frappe/core/page/dashboard/dashboard.js
index 511aac7010..88bfba9e84 100644
--- a/frappe/core/page/dashboard/dashboard.js
+++ b/frappe/core/page/dashboard/dashboard.js
@@ -59,7 +59,7 @@ class Dashboard {
}
show_dashboard(current_dashboard_name) {
- if(this.dashboard_name !== current_dashboard_name) {
+ if (this.dashboard_name !== current_dashboard_name) {
this.dashboard_name = current_dashboard_name;
let title = this.dashboard_name;
if (!this.dashboard_name.toLowerCase().includes(__('dashboard'))) {
@@ -76,30 +76,42 @@ class Dashboard {
}
refresh() {
- this.get_dashboard_doc().then((doc) => {
- this.dashboard_doc = doc;
- this.charts = this.dashboard_doc.charts
- .map(chart => {
- return {
- chart_name: chart.chart,
- label: chart.chart,
- ...chart
- }
- });
+ this.get_permitted_dashboard_charts().then(charts => {
+ if (!charts.length) {
+ frappe.msgprint(__('No Permitted Charts on this Dashboard'), __('No Permitted Charts'))
+ }
- this.chart_group = new frappe.widget.WidgetGroup({
- title: null,
- container: this.container,
- type: "chart",
- columns: 2,
- allow_sorting: false,
- widgets: this.charts,
- });
+ frappe.dashboard_utils.get_dashboard_settings().then((settings) => {
+ let chart_config = settings.chart_config? JSON.parse(settings.chart_config): {};
+ this.charts =
+ charts.map(chart => {
+ return {
+ chart_name: chart.chart,
+ label: chart.chart,
+ chart_settings: chart_config[chart.chart] || {},
+ ...chart
+ }
+ });
+ this.chart_group = new frappe.widget.WidgetGroup({
+ title: null,
+ container: this.container,
+ type: "chart",
+ columns: 2,
+ allow_sorting: false,
+ widgets: this.charts,
+ });
+ })
});
}
- get_dashboard_doc() {
- return frappe.model.with_doc('Dashboard', this.dashboard_name);
+ get_permitted_dashboard_charts() {
+ return frappe.xcall(
+ 'frappe.desk.doctype.dashboard.dashboard.get_permitted_charts',
+ {
+ dashboard_name: this.dashboard_name
+ }).then(charts => {
+ return charts;
+ });
}
set_dropdown() {
diff --git a/frappe/core/page/dashboard/dashboard.json b/frappe/core/page/dashboard/dashboard.json
index 891dcb26f8..58fda5a34c 100644
--- a/frappe/core/page/dashboard/dashboard.json
+++ b/frappe/core/page/dashboard/dashboard.json
@@ -4,7 +4,7 @@
"docstatus": 0,
"doctype": "Page",
"idx": 0,
- "modified": "2019-01-08 19:19:48.073410",
+ "modified": "2020-03-26 13:30:44.603948",
"modified_by": "Administrator",
"module": "Core",
"name": "dashboard",
diff --git a/frappe/core/utils.py b/frappe/core/utils.py
index 55767ffe34..55cfbc34d7 100644
--- a/frappe/core/utils.py
+++ b/frappe/core/utils.py
@@ -67,3 +67,19 @@ def find_all(list_of_dict, match_function):
if match_function(entry):
found.append(entry)
return found
+
+def ljust_list(_list, length, fill_word=None):
+ """
+ Similar to ljust but for list.
+
+ Usage:
+ $ ljust_list([1, 2, 3], 5)
+ > [1, 2, 3, None, None]
+ """
+ # make a copy to avoid mutation of passed list
+ _list = list(_list)
+ fill_length = length - len(_list)
+ if fill_length > 0:
+ _list.extend([fill_word] * fill_length)
+
+ return _list
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index ef84114745..1cb03355c6 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -146,8 +146,9 @@ class Workspace:
charts = charts + self.extended_charts
for chart in charts:
- chart.label = chart.label if chart.label else chart.chart_name
- all_charts.append(chart)
+ if frappe.has_permission('Dashboard Chart', doc=chart.chart_name):
+ chart.label = chart.label if chart.label else chart.chart_name
+ all_charts.append(chart)
return all_charts
diff --git a/frappe/desk/doctype/dashboard/dashboard.json b/frappe/desk/doctype/dashboard/dashboard.json
index 239f35bea8..c177ee70ac 100644
--- a/frappe/desk/doctype/dashboard/dashboard.json
+++ b/frappe/desk/doctype/dashboard/dashboard.json
@@ -34,7 +34,7 @@
}
],
"links": [],
- "modified": "2020-01-26 20:00:10.069817",
+ "modified": "2020-03-25 21:09:37.080132",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard",
@@ -51,6 +51,27 @@
"role": "System Manager",
"share": 1,
"write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Dashboard Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "All",
+ "share": 1
}
],
"quick_entry": 1,
diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py
index c8f22d29b9..5c344956bf 100644
--- a/frappe/desk/doctype/dashboard/dashboard.py
+++ b/frappe/desk/doctype/dashboard/dashboard.py
@@ -12,3 +12,12 @@ class Dashboard(Document):
# make all other dashboards non-default
frappe.db.sql('''update
tabDashboard set is_default = 0 where name != %s''', self.name)
+
+@frappe.whitelist()
+def get_permitted_charts(dashboard_name):
+ permitted_charts = []
+ dashboard = frappe.get_doc('Dashboard', dashboard_name)
+ for chart in dashboard.charts:
+ if frappe.has_permission('Dashboard Chart', doc=chart.chart):
+ permitted_charts.append(chart)
+ return permitted_charts
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
index 0a017a0de2..9652ae3945 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
@@ -215,7 +215,7 @@
}
],
"links": [],
- "modified": "2020-03-13 19:19:37.162771",
+ "modified": "2020-03-31 16:00:01.987059",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",
@@ -232,6 +232,27 @@
"role": "System Manager",
"share": 1,
"write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Dashboard Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "All",
+ "share": 1
}
],
"sort_field": "modified",
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
index f01c976b9c..b2a6f0a0ff 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
@@ -10,8 +10,51 @@ import json
from frappe.core.page.dashboard.dashboard import cache_source, get_from_date_from_timespan
from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate, get_datetime
from frappe.model.naming import append_number_if_name_exists
+from frappe.boot import get_allowed_reports
from frappe.model.document import Document
+
+def get_permission_query_conditions(user):
+
+ if not user:
+ user = frappe.session.user
+
+ if user == 'Administrator':
+ return
+
+ roles = frappe.get_roles(user)
+ if "System Manager" in roles:
+ return None
+
+ allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read())
+ allowed_reports = tuple([key.encode('UTF8') for key in get_allowed_reports()])
+
+ return '''
+ `tabDashboard Chart`.`document_type` in {allowed_doctypes}
+ or `tabDashboard Chart`.`report_name` in {allowed_reports}
+ '''.format(
+ allowed_doctypes=allowed_doctypes,
+ allowed_reports=allowed_reports
+ )
+
+
+def has_permission(doc, ptype, user):
+ roles = frappe.get_roles(user)
+ if "System Manager" in roles:
+ return True
+
+
+ if doc.chart_type == 'Report':
+ allowed_reports = tuple([key.encode('UTF8') for key in get_allowed_reports()])
+ if doc.report_name in allowed_reports:
+ return True
+ else:
+ allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read())
+ if doc.document_type in allowed_doctypes:
+ return True
+
+ return False
+
@frappe.whitelist()
@cache_source
def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None,
diff --git a/frappe/desk/doctype/dashboard_settings/__init__.py b/frappe/desk/doctype/dashboard_settings/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/doctype/dashboard_settings/dashboard_settings.js b/frappe/desk/doctype/dashboard_settings/dashboard_settings.js
new file mode 100644
index 0000000000..8e7966366d
--- /dev/null
+++ b/frappe/desk/doctype/dashboard_settings/dashboard_settings.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Dashboard Settings', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/desk/doctype/dashboard_settings/dashboard_settings.json b/frappe/desk/doctype/dashboard_settings/dashboard_settings.json
new file mode 100644
index 0000000000..e1eb75db47
--- /dev/null
+++ b/frappe/desk/doctype/dashboard_settings/dashboard_settings.json
@@ -0,0 +1,51 @@
+{
+ "actions": [],
+ "autoname": "Prompt",
+ "creation": "2020-03-31 19:41:45.785014",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "user",
+ "chart_config"
+ ],
+ "fields": [
+ {
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "label": "User",
+ "options": "User",
+ "read_only": 1
+ },
+ {
+ "fieldname": "chart_config",
+ "fieldtype": "Code",
+ "label": "Chart Configuration",
+ "options": "JSON",
+ "read_only": 1
+ }
+ ],
+ "in_create": 1,
+ "links": [],
+ "modified": "2020-04-01 00:07:26.489561",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Dashboard Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "All",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "read_only": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/dashboard_settings/dashboard_settings.py b/frappe/desk/doctype/dashboard_settings/dashboard_settings.py
new file mode 100644
index 0000000000..4697d897fc
--- /dev/null
+++ b/frappe/desk/doctype/dashboard_settings/dashboard_settings.py
@@ -0,0 +1,43 @@
+# -*- 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
+import frappe
+import json
+
+class DashboardSettings(Document):
+ pass
+
+
+@frappe.whitelist()
+def create_dashboard_settings(user):
+ if not frappe.db.exists("Dashboard Settings", user):
+ doc = frappe.new_doc('Dashboard Settings')
+ doc.name = user
+ doc.insert(ignore_permissions=True)
+ frappe.db.commit()
+ return doc
+
+def get_permission_query_conditions(user):
+ if not user: user = frappe.session.user
+
+ return '''(`tabDashboard Settings`.name = '{user}')'''.format(user=user)
+
+@frappe.whitelist()
+def save_chart_config(reset, config, chart_name):
+ reset = frappe.parse_json(reset)
+ doc = frappe.get_doc('Dashboard Settings', frappe.session.user)
+ chart_config = frappe.parse_json(doc.chart_config) or {}
+
+ if reset:
+ chart_config[chart_name] = {}
+ else:
+ config = frappe.parse_json(config)
+ if not chart_name in chart_config:
+ chart_config[chart_name] = {}
+ chart_config[chart_name].update(config)
+
+ frappe.db.set_value('Dashboard Settings', frappe.session.user, 'chart_config', json.dumps(chart_config))
\ No newline at end of file
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index d210af02fd..aaf859e7fd 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -8,7 +8,7 @@ import os, json
from frappe import _
from frappe.modules import scrub, get_module_path
-from frappe.utils import flt, cint, get_html_format, cstr, get_url_to_form
+from frappe.utils import flt, cint, get_html_format, get_url_to_form
from frappe.model.utils import render_include
from frappe.translate import send_translations
import frappe.desk.reportview
@@ -16,6 +16,7 @@ from frappe.permissions import get_role_permissions
from six import string_types, iteritems
from datetime import timedelta
from frappe.utils import gzip_decompress
+from frappe.core.utils import ljust_list
def get_report_doc(report_name):
doc = frappe.get_doc("Report", report_name)
@@ -42,44 +43,32 @@ def get_report_doc(report_name):
return doc
-def generate_report_result(report, filters=None, user=None):
- status = None
- if not user:
- user = frappe.session.user
- if not filters:
- filters = []
+def generate_report_result(report, filters=None, user=None, custom_columns=None):
+ user = user or frappe.session.user
+ filters = filters or []
if filters and isinstance(filters, string_types):
filters = json.loads(filters)
- columns, result, message, chart, report_summary, skip_total_row = [], [], None, None, None, 0
+
+ res = []
+
if report.report_type == "Query Report":
- if not report.query:
- status = "error"
- frappe.msgprint(_("Must specify a Query to run"), raise_exception=True)
-
- if not report.query.lower().startswith("select"):
- status = "error"
- frappe.msgprint(_("Query must be a SELECT"), raise_exception=True)
-
- result = [list(t) for t in frappe.db.sql(report.query, filters)]
- columns = [cstr(c[0]) for c in frappe.db.get_description()]
+ res = report.execute_query_report(filters)
elif report.report_type == 'Script Report':
res = report.execute_script_report(filters)
- columns, result = res[0], res[1]
- if len(res) > 2:
- message = res[2]
- if len(res) > 3:
- chart = res[3]
- if len(res) > 4:
- report_summary = res[4]
- if len(res) > 5:
- skip_total_row = cint(res[5])
+ columns, result, message, chart, report_summary, skip_total_row = \
+ ljust_list(res, 6)
if report.custom_columns:
columns = json.loads(report.custom_columns)
result = add_data_to_custom_columns(columns, result)
+ if custom_columns:
+ result = add_data_to_custom_columns(custom_columns, result)
+
+ for custom_column in custom_columns:
+ columns.insert(custom_column['insert_after_index'] + 1, custom_column)
if result:
result = get_filtered_data(report.ref_doctype, columns, result, user)
@@ -93,8 +82,8 @@ def generate_report_result(report, filters=None, user=None):
"message": message,
"chart": chart,
"report_summary": report_summary,
- "skip_total_row": skip_total_row,
- "status": status,
+ "skip_total_row": skip_total_row or 0,
+ "status": None,
"execution_time": frappe.cache().hget('report_execution_time', report.name) or 0
}
@@ -161,7 +150,7 @@ def get_script(report_name):
@frappe.whitelist()
@frappe.read_only()
-def run(report_name, filters=None, user=None, ignore_prepared_report=False):
+def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None):
report = get_report_doc(report_name)
if not user:
@@ -183,7 +172,7 @@ def run(report_name, filters=None, user=None, ignore_prepared_report=False):
dn = ""
result = get_prepared_report_result(report, filters, dn, user)
else:
- result = generate_report_result(report, filters, user)
+ result = generate_report_result(report, filters, user, custom_columns)
result["add_total_row"] = report.add_total_row and not result.get('skip_total_row', False)
@@ -294,6 +283,8 @@ def export_query():
if isinstance(data.get("file_format_type"), string_types):
file_format_type = data["file_format_type"]
+ custom_columns = frappe.parse_json(data["custom_columns"])
+
include_indentation = data["include_indentation"]
if isinstance(data.get("visible_idx"), string_types):
visible_idx = json.loads(data.get("visible_idx"))
@@ -301,7 +292,7 @@ def export_query():
visible_idx = None
if file_format_type == "Excel":
- data = run(report_name, filters)
+ data = run(report_name, filters, custom_columns=custom_columns)
data = frappe._dict(data)
if not data.columns:
frappe.respond_as_web_page(_("No data to export"),
diff --git a/frappe/exceptions.py b/frappe/exceptions.py
index 3d63f4b2b4..732fc39e9a 100644
--- a/frappe/exceptions.py
+++ b/frappe/exceptions.py
@@ -78,6 +78,7 @@ class TimestampMismatchError(ValidationError): pass
class EmptyTableError(ValidationError): pass
class LinkExistsError(ValidationError): pass
class InvalidEmailAddressError(ValidationError): pass
+class InvalidPhoneNumberError(ValidationError): pass
class TemplateNotFoundError(ValidationError): pass
class UniqueValidationError(ValidationError): pass
class AppNotInstalledError(ValidationError): pass
diff --git a/frappe/hooks.py b/frappe/hooks.py
index c44c05fdf4..4f65303be9 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -86,7 +86,9 @@ permission_query_conditions = {
"Event": "frappe.desk.doctype.event.event.get_permission_query_conditions",
"ToDo": "frappe.desk.doctype.todo.todo.get_permission_query_conditions",
"User": "frappe.core.doctype.user.user.get_permission_query_conditions",
+ "Dashboard Settings": "frappe.desk.doctype.dashboard_settings.dashboard_settings.get_permission_query_conditions",
"Notification Log": "frappe.desk.doctype.notification_log.notification_log.get_permission_query_conditions",
+ "Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.get_permission_query_conditions",
"Notification Settings": "frappe.desk.doctype.notification_settings.notification_settings.get_permission_query_conditions",
"Note": "frappe.desk.doctype.note.note.get_permission_query_conditions",
"Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.get_permission_query_conditions",
@@ -101,6 +103,7 @@ has_permission = {
"ToDo": "frappe.desk.doctype.todo.todo.has_permission",
"User": "frappe.core.doctype.user.user.has_permission",
"Note": "frappe.desk.doctype.note.note.has_permission",
+ "Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.has_permission",
"Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.has_permission",
"Contact": "frappe.contacts.address_and_contact.has_permission",
"Address": "frappe.contacts.address_and_contact.has_permission",
diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py
index c0f12df04a..558f7117c0 100644
--- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py
+++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py
@@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe
-from frappe import _
+from frappe import _, safe_encode
from frappe.model.document import Document
@@ -19,7 +19,7 @@ class LDAPSettings(Document):
else:
frappe.throw(_("LDAP Search String needs to end with a placeholder, eg sAMAccountName={0}"))
- def connect_to_ldap(self, base_dn, password):
+ def connect_to_ldap(self, base_dn, password, read_only=True):
try:
import ldap3
import ssl
@@ -44,7 +44,7 @@ class LDAPSettings(Document):
user=base_dn,
password=password,
auto_bind=bind_type,
- read_only=True,
+ read_only=read_only,
raise_exceptions=True)
return conn
@@ -170,6 +170,36 @@ class LDAPSettings(Document):
else:
frappe.throw(_("Invalid username or password"))
+ def reset_password(self, user, password, logout_sessions=False):
+ from ldap3 import HASHED_SALTED_SHA, MODIFY_REPLACE
+ from ldap3.utils.hashed import hashed
+
+ search_filter = "({0}={1})".format(self.ldap_email_field, user)
+
+ conn = self.connect_to_ldap(self.base_dn, self.get_password(raise_exception=False),
+ read_only=False)
+
+ if conn.search(
+ search_base=self.organizational_unit,
+ search_filter=search_filter,
+ attributes=self.get_ldap_attributes()
+ ):
+ if conn.entries and conn.entries[0]:
+ entry_dn = conn.entries[0].entry_dn
+ hashed_password = hashed(HASHED_SALTED_SHA, safe_encode(password))
+ changes = {'userPassword': [(MODIFY_REPLACE, [hashed_password])]}
+ if conn.modify(entry_dn, changes=changes):
+ if logout_sessions:
+ from frappe.sessions import clear_sessions
+ clear_sessions(user=user, force=True)
+ frappe.msgprint(_("Password changed successfully."))
+ else:
+ frappe.throw(_("Failed to change password."))
+ else:
+ frappe.throw(_("No Entry for the User {0} found within LDAP!").format(user))
+ else:
+ frappe.throw(_("No LDAP User found for email: {0}").format(user))
+
def convert_ldap_entry_to_dict(self, user_entry):
# support multiple email values
@@ -211,3 +241,11 @@ def login():
# because of a GET request!
frappe.db.commit()
+
+
+@frappe.whitelist()
+def reset_password(user, password, logout):
+ ldap = frappe.get_doc("LDAP Settings")
+ if not ldap.enabled:
+ frappe.throw(_("LDAP is not enabled."))
+ ldap.reset_password(user, password, logout_sessions=int(logout))
diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py
index 1fe92d7a67..7af987f4bc 100644
--- a/frappe/model/__init__.py
+++ b/frappe/model/__init__.py
@@ -48,6 +48,7 @@ table_fields = ('Table', 'Table MultiSelect')
core_doctypes_list = ('DocType', 'DocField', 'DocPerm', 'DocType Action', 'DocType Link', 'User', 'Role', 'Has Role',
'Page', 'Module Def', 'Print Format', 'Report', 'Customize Form',
'Customize Form Field', 'Property Setter', 'Custom Field', 'Custom Script')
+data_field_options = ('Email', 'Phone')
def copytables(srctype, src, srcfield, tartype, tar, tarfield, srcfields, tarfields=[]):
if not tarfields:
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index 569cea9d5f..4af502f844 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -544,6 +544,23 @@ class BaseDocument(object):
frappe.throw(_('{0} {1} cannot be "{2}". It should be one of "{3}"').format(prefix, label,
value, comma_options))
+ def _validate_data_fields(self):
+ from frappe.core.doctype.user.user import STANDARD_USERS
+
+ # data_field options defined in frappe.model.data_field_options
+ for data_field in self.meta.get_data_fields():
+ data = self.get(data_field.fieldname)
+ data_field_options = data_field.get("options")
+
+ if data_field_options == "Email":
+ if (self.owner in STANDARD_USERS) and (data in STANDARD_USERS):
+ return
+ for email_address in frappe.utils.split_emails(data):
+ frappe.utils.validate_email_address(email_address, throw=True)
+
+ if data_field_options == "Phone":
+ frappe.utils.validate_phone_number(data, throw=True)
+
def _validate_constants(self):
if frappe.flags.in_import or self.is_new() or self.flags.ignore_validate_constants:
return
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 66dd7e3c58..03b21ea667 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -468,6 +468,7 @@ class Document(BaseDocument):
def _validate(self):
self._validate_mandatory()
+ self._validate_data_fields()
self._validate_selects()
self._validate_length()
self._extract_images_from_text_editor()
@@ -477,6 +478,7 @@ class Document(BaseDocument):
children = self.get_all_children()
for d in children:
+ d._validate_data_fields()
d._validate_selects()
d._validate_length()
d._extract_images_from_text_editor()
diff --git a/frappe/model/meta.py b/frappe/model/meta.py
index 84c96d0566..9c71f8c0b1 100644
--- a/frappe/model/meta.py
+++ b/frappe/model/meta.py
@@ -128,6 +128,9 @@ class Meta(Document):
def get_link_fields(self):
return self.get("fields", {"fieldtype": "Link", "options":["!=", "[Select]"]})
+ def get_data_fields(self):
+ return self.get("fields", {"fieldtype": "Data"})
+
def get_dynamic_link_fields(self):
if not hasattr(self, '_dynamic_link_fields'):
self._dynamic_link_fields = self.get("fields", {"fieldtype": "Dynamic Link"})
diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py
index 3f3711af9d..4384e7c8f5 100644
--- a/frappe/model/workflow.py
+++ b/frappe/model/workflow.py
@@ -194,7 +194,7 @@ def bulk_workflow_approval(docnames, doctype, action):
from collections import defaultdict
# dictionaries for logging
- errored_transactions = defaultdict(list)
+ failed_transactions = defaultdict(list)
successful_transactions = defaultdict(list)
# WARN: message log is cleared
@@ -215,7 +215,7 @@ def bulk_workflow_approval(docnames, doctype, action):
if e.args:
message += " : {0}".format(e.args[0])
message_dict = {"docname": docname, "message": message}
- errored_transactions[docname].append(message_dict)
+ failed_transactions[docname].append(message_dict)
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), "Workflow {0} threw an error for {1} {2}".format(action, doctype, docname))
@@ -228,20 +228,20 @@ def bulk_workflow_approval(docnames, doctype, action):
message_dict = {"docname": docname, "message": message.get("message")}
if message.get("raise_exception", False):
- errored_transactions[docname].append(message_dict)
+ failed_transactions[docname].append(message_dict)
else:
successful_transactions[docname].append(message_dict)
else:
successful_transactions[docname].append({"docname": docname, "message": None})
- if errored_transactions and successful_transactions:
+ if failed_transactions and successful_transactions:
indicator = "orange"
- elif errored_transactions:
+ elif failed_transactions:
indicator = "red"
else:
indicator = "green"
- print_workflow_log(errored_transactions, _("Errored Transactions"), doctype, indicator)
+ print_workflow_log(failed_transactions, _("Failed Transactions"), doctype, indicator)
print_workflow_log(successful_transactions, _("Successful Transactions"), doctype, indicator)
def print_workflow_log(messages, title, doctype, indicator):
diff --git a/frappe/permissions.py b/frappe/permissions.py
index a0d1677fac..0d766aec8d 100644
--- a/frappe/permissions.py
+++ b/frappe/permissions.py
@@ -307,7 +307,7 @@ def has_controller_permissions(doc, ptype, user=None):
return None
def get_doctypes_with_read():
- return list(set([p.parent for p in get_valid_perms()]))
+ return list(set([p.parent if type(p.parent) == str else p.parent.encode('UTF8') for p in get_valid_perms()]))
def get_valid_perms(doctype=None, user=None):
'''Get valid permissions for the current user from DocPerm and Custom DocPerm'''
diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js
index 20f78ec248..90cf784dd4 100644
--- a/frappe/public/js/frappe/form/controls/base_control.js
+++ b/frappe/public/js/frappe/form/controls/base_control.js
@@ -152,6 +152,7 @@ frappe.ui.form.Control = Class.extend({
() => me.set_model_value(value),
() => {
me.set_mandatory && me.set_mandatory(value);
+ me.set_invalid && me.set_invalid();
if(me.df.change || me.df.onchange) {
// onchange event specified in df
diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js
index 8a8ac271c7..0dbaaeb63c 100644
--- a/frappe/public/js/frappe/form/controls/base_input.js
+++ b/frappe/public/js/frappe/form/controls/base_input.js
@@ -179,6 +179,9 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
set_mandatory: function(value) {
this.$wrapper.toggleClass("has-error", (this.df.reqd && is_null(value)) ? true : false);
},
+ set_invalid: function () {
+ this.$wrapper.toggleClass("has-error", (this.df.invalid ? true : false));
+ },
set_bold: function() {
if(this.$input) {
this.$input.toggleClass("bold", !!(this.df.bold || this.df.reqd));
diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js
index 6dc8c3d387..a7f0050d65 100644
--- a/frappe/public/js/frappe/form/controls/data.js
+++ b/frappe/public/js/frappe/form/controls/data.js
@@ -87,56 +87,29 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({
return val==null ? "" : val;
},
validate: function(v) {
+ if (!v) {
+ return '';
+ }
if(this.df.is_filter) {
return v;
}
if(this.df.options == 'Phone') {
- if(v+''=='') {
- return '';
- }
- var v1 = '';
- // phone may start with + and must only have numbers later, '-' and ' ' are stripped
- v = v.replace(/ /g, '').replace(/-/g, '').replace(/\(/g, '').replace(/\)/g, '');
-
- // allow initial +,0,00
- if(v && v.substr(0,1)=='+') {
- v1 = '+'; v = v.substr(1);
- }
- if(v && v.substr(0,2)=='00') {
- v1 += '00'; v = v.substr(2);
- }
- if(v && v.substr(0,1)=='0') {
- v1 += '0'; v = v.substr(1);
- }
- v1 += cint(v) + '';
- return v1;
+ this.df.invalid = !validate_phone(v);
+ return v;
} else if(this.df.options == 'Email') {
- if(v+''=='') {
- return '';
- }
-
var email_list = frappe.utils.split_emails(v);
if (!email_list) {
- // invalid email
return '';
} else {
- var invalid_email = false;
+ let email_invalid = false;
email_list.forEach(function(email) {
if (!validate_email(email)) {
- frappe.msgprint(__("Invalid Email: {0}", [email]));
- invalid_email = true;
+ email_invalid = true;
}
});
-
- if (invalid_email) {
- // at least 1 invalid email
- return '';
- } else {
- // all good
- return v;
- }
+ this.df.invalid = email_invalid;
+ return v;
}
-
} else {
return v;
}
diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js
index d27c65548d..beec168dfd 100644
--- a/frappe/public/js/frappe/form/footer/timeline.js
+++ b/frappe/public/js/frappe/form/footer/timeline.js
@@ -561,10 +561,9 @@ frappe.ui.form.Timeline = class Timeline {
}
let updater_reference_link = null;
-
- if (!$.isEmptyObject(data.updater_reference)) {
+ let updater_reference = data.updater_reference;
+ if (!$.isEmptyObject(updater_reference)) {
let label = updater_reference.label || __('via {0}', [updater_reference.doctype]);
- let updater_reference = data.updater_reference;
updater_reference_link = frappe.utils.get_form_link(
updater_reference.doctype,
updater_reference.docname,
diff --git a/frappe/public/js/frappe/utils/dashboard_utils.js b/frappe/public/js/frappe/utils/dashboard_utils.js
index 7e64f5c143..66f3d5e21f 100644
--- a/frappe/public/js/frappe/utils/dashboard_utils.js
+++ b/frappe/public/js/frappe/utils/dashboard_utils.js
@@ -54,5 +54,26 @@ frappe.dashboard_utils = {
} else {
return Promise.resolve();
}
+ },
+
+ get_dashboard_settings() {
+ return frappe.model.with_doc('Dashboard Settings', frappe.session.user).then(settings => {
+ if (!settings) {
+ return this.create_dashboard_settings().then(settings => {
+ return settings;
+ });
+ } else {
+ return settings;
+ }
+ });
+ },
+
+ create_dashboard_settings() {
+ return frappe.xcall(
+ 'frappe.desk.doctype.dashboard_settings.dashboard_settings.create_dashboard_settings',
+ {user: frappe.session.user}
+ ).then(settings => {
+ return settings;
+ });
}
};
\ No newline at end of file
diff --git a/frappe/public/js/frappe/utils/datatype.js b/frappe/public/js/frappe/utils/datatype.js
index 0f73526e04..16f87b872f 100644
--- a/frappe/public/js/frappe/utils/datatype.js
+++ b/frappe/public/js/frappe/utils/datatype.js
@@ -44,6 +44,10 @@ window.validate_email = function(txt) {
return frappe.utils.validate_type(txt, "email");
};
+window.validate_phone = function(txt) {
+ return frappe.utils.validate_type(txt, "phone");
+};
+
window.nth = function(number) {
number = cint(number);
var s = 'th';
diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js
index 278c80897e..1af37c1f60 100644
--- a/frappe/public/js/frappe/utils/utils.js
+++ b/frappe/public/js/frappe/utils/utils.js
@@ -232,6 +232,9 @@ Object.assign(frappe.utils, {
var regExp;
switch ( type ) {
+ case "phone":
+ regExp = /^([0-9\ \+\_\-\,\.\*\#\(\)]){1,20}$/;
+ break;
case "number":
regExp = /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/;
break;
diff --git a/frappe/public/js/frappe/views/desktop/desktop.js b/frappe/public/js/frappe/views/desktop/desktop.js
index 54a25c3771..d0993e2e8c 100644
--- a/frappe/public/js/frappe/views/desktop/desktop.js
+++ b/frappe/public/js/frappe/views/desktop/desktop.js
@@ -165,11 +165,18 @@ class DesktopPage {
this.allow_customization = this.data.allow_customization || false;
- !this.sections["onboarding"] &&
- this.data.charts.items.length &&
- this.make_charts();
- this.data.shortcuts.items.length && this.make_shortcuts();
- this.data.cards.items.length && this.make_cards();
+ let create_shortcuts_and_cards = () => {
+ this.data.shortcuts.items.length && this.make_shortcuts();
+ this.data.cards.items.length && this.make_cards();
+ };
+
+ if (!this.sections["onboarding"] && this.data.charts.items.length) {
+ this.make_charts().then(() => {
+ create_shortcuts_and_cards();
+ });
+ } else {
+ create_shortcuts_and_cards();
+ }
});
}
@@ -224,13 +231,22 @@ class DesktopPage {
}
make_charts() {
- this.sections["charts"] = new frappe.widget.WidgetGroup({
- title: this.data.charts.label || `${this.page_name} Dashboard`,
- container: this.page,
- type: "chart",
- columns: 1,
- allow_sorting: false,
- widgets: this.data.charts.items
+ return frappe.dashboard_utils.get_dashboard_settings().then(settings => {
+ let chart_config = settings.chart_config? JSON.parse(settings.chart_config): {};
+ if (this.data.charts.items) {
+ this.data.charts.items.map(chart => {
+ chart.chart_settings = chart_config[chart.chart_name] || {};
+ });
+ }
+
+ this.sections["charts"] = new frappe.widget.WidgetGroup({
+ title: this.data.charts.label || `${this.page_name} Dashboard`,
+ container: this.page,
+ type: "chart",
+ columns: 1,
+ allow_sorting: false,
+ widgets: this.data.charts.items
+ });
});
}
diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js
index 1d7065e70d..2276bc07d6 100644
--- a/frappe/public/js/frappe/views/reports/query_report.js
+++ b/frappe/public/js/frappe/views/reports/query_report.js
@@ -616,6 +616,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
prepare_report_data(data) {
this.raw_data = data;
this.columns = this.prepare_columns(data.columns);
+ this.custom_columns = [];
this.data = this.prepare_data(data.result);
this.linked_doctypes = this.get_linked_doctypes();
this.tree_report = this.data.some(d => 'indent' in d);
@@ -1110,6 +1111,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
const args = {
cmd: 'frappe.desk.query_report.export_query',
report_name: this.report_name,
+ custom_columns: this.custom_columns.length? this.custom_columns: [],
file_format_type: file_format,
filters: filters,
visible_idx,
@@ -1275,16 +1277,20 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
primary_action: (values) => {
const custom_columns = [];
let df = frappe.meta.get_docfield(values.doctype, values.field);
+ const insert_after_index = this.columns
+ .findIndex(column => column.label === values.insert_after);
custom_columns.push({
fieldname: df.fieldname,
fieldtype: df.fieldtype,
label: df.label,
+ insert_after_index: insert_after_index,
link_field: this.doctype_field_map[values.doctype],
doctype: values.doctype,
options: df.fieldtype === "Link" ? df.options : undefined,
width: 100
});
+ this.custom_columns = this.custom_columns.concat(custom_columns);
frappe.call({
method: 'frappe.desk.query_report.get_data_for_custom_field',
args: {
@@ -1294,7 +1300,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
callback: (r) => {
const custom_data = r.message;
const link_field = this.doctype_field_map[values.doctype];
- this.add_custom_column(custom_columns, custom_data, link_field, values.field, values.insert_after);
+
+ this.add_custom_column(custom_columns, custom_data, link_field, values.field, insert_after_index);
d.hide();
}
});
@@ -1369,11 +1376,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
}
}
- add_custom_column(custom_column, custom_data, link_field, column_field, insert_after) {
+ add_custom_column(custom_column, custom_data, link_field, column_field, insert_after_index) {
const column = this.prepare_columns(custom_column);
- const insert_after_index = this.columns
- .findIndex(column => column.label === insert_after);
this.columns.splice(insert_after_index + 1, 0, column[0]);
this.data.forEach(row => {
diff --git a/frappe/public/js/frappe/widgets/chart_widget.js b/frappe/public/js/frappe/widgets/chart_widget.js
index 3388890776..174f0ae666 100644
--- a/frappe/public/js/frappe/widgets/chart_widget.js
+++ b/frappe/public/js/frappe/widgets/chart_widget.js
@@ -62,6 +62,9 @@ export default class ChartWidget extends Widget {
make_chart() {
this.get_settings().then(() => {
+ if (!this.chart_settings) {
+ this.chart_settings = {};
+ }
this.setup_container();
this.prepare_chart_object();
this.action_area.empty();
@@ -89,7 +92,7 @@ export default class ChartWidget extends Widget {
render_time_series_filters() {
let filters = [
{
- label: this.chart_doc.timespan,
+ label: this.chart_settings.timespan || this.chart_doc.timespan,
options: [
"Select Date Range",
"Last Year",
@@ -114,15 +117,22 @@ export default class ChartWidget extends Widget {
this.head.css('flex-direction', "row");
}
+ this.save_chart_config_for_user({
+ 'timespan': this.selected_timespan,
+ 'from_date': null,
+ 'to_date': null
+
+ });
this.fetch_and_update_chart();
}
}
},
{
- label: this.chart_doc.time_interval,
+ label: this.chart_settings.time_interval || this.chart_doc.time_interval,
options: ["Yearly", "Quarterly", "Monthly", "Weekly", "Daily"],
action: selected_item => {
this.selected_time_interval = selected_item;
+ this.save_chart_config_for_user({'time_interval': this.selected_time_interval});
this.fetch_and_update_chart();
}
}
@@ -138,10 +148,10 @@ export default class ChartWidget extends Widget {
fetch_and_update_chart() {
this.args = {
- timespan: this.selected_timespan,
- time_interval: this.selected_time_interval,
- from_date: this.selected_from_date,
- to_date: this.selected_to_date
+ timespan: this.selected_timespan || this.chart_settings.timespan,
+ time_interval: this.selected_time_interval || this.chart_settings.time_interval,
+ from_date: this.selected_from_date || this.chart_settings.from_date,
+ to_date: this.selected_to_date || this.chart_settings.to_date
};
this.fetch(this.filters, true, this.args).then(data => {
@@ -176,16 +186,19 @@ export default class ChartWidget extends Widget {
fieldname: "from_date",
placeholder: "Date Range",
input_class: "input-xs",
+ default: [this.chart_settings.from_date, this.chart_settings.to_date],
reqd: 1,
change: () => {
let selected_date_range = this.date_range_field.get_value();
this.selected_from_date = selected_date_range[0];
this.selected_to_date = selected_date_range[1];
- if (
- selected_date_range &&
- selected_date_range.length == 2
- ) {
+ if (selected_date_range && selected_date_range.length == 2) {
+ this.save_chart_config_for_user({
+ 'timespan': this.selected_timespan,
+ 'from_date': this.selected_from_date,
+ 'to_date': this.selected_to_date,
+ });
this.fetch_and_update_chart();
}
}
@@ -235,7 +248,7 @@ export default class ChartWidget extends Widget {
}
},
{
- label: __("Edit..."),
+ label: __("Edit"),
action: "action-edit",
handler: () => {
frappe.set_route(
@@ -244,6 +257,15 @@ export default class ChartWidget extends Widget {
this.chart_doc.name
);
}
+ },
+ {
+ label: __("Reset Chart"),
+ action: "action-list",
+ handler: () => {
+ this.reset_chart();
+ delete this.dashboard_chart;
+ this.make_chart();
+ }
}
];
@@ -334,6 +356,7 @@ export default class ChartWidget extends Widget {
} else {
me.filters = values;
}
+ me.save_chart_config_for_user({'filters': me.filters});
me.fetch_and_update_chart();
}
},
@@ -350,6 +373,21 @@ export default class ChartWidget extends Widget {
dialog.set_values(this.filters);
}
+ reset_chart() {
+ this.save_chart_config_for_user(null, 1);
+ this.chart_settings = {};
+ this.filters = null;
+ }
+
+ save_chart_config_for_user(config, reset=0) {
+ Object.assign(this.chart_settings, config);
+ frappe.xcall('frappe.desk.doctype.dashboard_settings.dashboard_settings.save_chart_config', {
+ 'reset': reset,
+ 'config': this.chart_settings,
+ 'chart_name': this.chart_doc.chart_name
+ });
+ }
+
create_filter_group_and_add_filters(parent) {
this.filter_group = new frappe.ui.FilterGroup({
parent: parent,
@@ -406,10 +444,10 @@ export default class ChartWidget extends Widget {
filters: filters,
refresh: refresh ? 1 : 0,
time_interval:
- args && args.time_interval ? args.time_interval : null,
- timespan: args && args.timespan ? args.timespan : null,
- from_date: args && args.from_date ? args.from_date : null,
- to_date: args && args.to_date ? args.to_date : null
+ args && args.time_interval? args.time_interval: null,
+ timespan: args && args.timespan? args.timespan: null,
+ from_date: args && args.from_date? args.from_date: null,
+ to_date: args && args.to_date? args.to_date: null
};
}
return frappe.xcall(method, args);
@@ -481,8 +519,9 @@ export default class ChartWidget extends Widget {
}
prepare_chart_object() {
+ let saved_filters = this.chart_settings.filters || null;
this.filters =
- this.filters || JSON.parse(this.chart_doc.filters_json || "[]");
+ saved_filters || this.filters || JSON.parse(this.chart_doc.filters_json || "[]");
}
get_settings() {
diff --git a/frappe/tests/test_global_search.py b/frappe/tests/test_global_search.py
index 01067c85dd..5cffbaaf64 100644
--- a/frappe/tests/test_global_search.py
+++ b/frappe/tests/test_global_search.py
@@ -191,3 +191,6 @@ class TestGlobalSearch(unittest.TestCase):
frappe.db.commit()
results = global_search.web_search('unsubscribe')
self.assertTrue('Unsubscribe' in results[0].content)
+ results = global_search.web_search(text='unsubscribe',
+ scope="manufacturing\" UNION ALL SELECT 1,2,3,4,doctype from __global_search")
+ self.assertTrue(results == [])
diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py
index 82e6ea1b45..649d3bf72c 100644
--- a/frappe/utils/__init__.py
+++ b/frappe/utils/__init__.py
@@ -75,11 +75,18 @@ def extract_email_id(email):
email_id = email_id.decode("utf-8", "ignore")
return email_id
-def validate_email_add(email_str, throw=False):
- """
- validate_email_add will be renamed to the validate_email_address in v12
- """
- return validate_email_address(email_str, throw=False)
+def validate_phone_number(phone_number, throw=False):
+ """Returns True if valid phone number"""
+ if not phone_number:
+ return False
+
+ phone_number = phone_number.strip()
+ match = re.match("([0-9\ \+\_\-\,\.\*\#\(\)]){1,20}$", phone_number)
+
+ if not match and throw:
+ frappe.throw(frappe._("{0} is not a valid Phone Number").format(phone_number), frappe.InvalidPhoneNumberError)
+
+ return bool(match)
def validate_email_address(email_str, throw=False):
"""Validates the email string"""
@@ -98,15 +105,15 @@ def validate_email_address(email_str, throw=False):
_valid = False
else:
- e = extract_email_id(e)
- match = re.match("[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", e.lower()) if e else None
+ email_id = extract_email_id(e)
+ match = re.match("[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", email_id.lower()) if email_id else None
if not match:
_valid = False
else:
matched = match.group(0)
if match:
- match = matched==e.lower()
+ match = matched==email_id.lower()
if not _valid:
if throw:
@@ -691,4 +698,4 @@ def get_html_for_route(route):
set_request(method='GET', path=route)
response = render.render()
html = frappe.safe_decode(response.get_data())
- return html
\ No newline at end of file
+ return html
diff --git a/frappe/utils/global_search.py b/frappe/utils/global_search.py
index 4b50745a74..3c4b9583f8 100644
--- a/frappe/utils/global_search.py
+++ b/frappe/utils/global_search.py
@@ -499,22 +499,29 @@ def web_search(text, scope=None, start=0, limit=20):
common_query = ''' SELECT `doctype`, `name`, `content`, `title`, `route`
FROM `__global_search`
WHERE {conditions}
- LIMIT {limit} OFFSET {start}'''
+ LIMIT %(limit)s OFFSET %(start)s'''
- scope_condition = '`route` like "{}%" AND '.format(scope) if scope else ''
+ scope_condition = '`route` like %(scope)s AND ' if scope else ''
published_condition = '`published` = 1 AND '
mariadb_conditions = postgres_conditions = ' '.join([published_condition, scope_condition])
# https://mariadb.com/kb/en/library/full-text-index-overview/#in-boolean-mode
text = '"{}"'.format(text)
- mariadb_conditions += 'MATCH(`content`) AGAINST ({} IN BOOLEAN MODE)'.format(frappe.db.escape(text))
- postgres_conditions += 'TO_TSVECTOR("content") @@ PLAINTO_TSQUERY({})'.format(frappe.db.escape(text))
+ mariadb_conditions += 'MATCH(`content`) AGAINST (%(text)s IN BOOLEAN MODE)'
+ postgres_conditions += 'TO_TSVECTOR("content") @@ PLAINTO_TSQUERY(%(text)s)'
+
+ values = {
+ "scope": "".join([scope, "%"]) if scope else '',
+ "limit": limit,
+ "start": start,
+ "text": text
+ }
result = frappe.db.multisql({
- 'mariadb': common_query.format(conditions=mariadb_conditions, limit=limit, start=start),
- 'postgres': common_query.format(conditions=postgres_conditions, limit=limit, start=start)
- }, as_dict=True)
- tmp_result=[]
+ 'mariadb': common_query.format(conditions=mariadb_conditions),
+ 'postgres': common_query.format(conditions=postgres_conditions)
+ }, values=values, as_dict=True)
+ tmp_result = []
for i in result:
if i in results or not results:
tmp_result.append(i)
diff --git a/frappe/utils/redis_wrapper.py b/frappe/utils/redis_wrapper.py
index b0c0990e85..385bac8e4a 100644
--- a/frappe/utils/redis_wrapper.py
+++ b/frappe/utils/redis_wrapper.py
@@ -43,7 +43,7 @@ class RedisWrapper(redis.Redis):
try:
if expires_in_sec:
- self.setex(key, expires_in_sec, pickle.dumps(val))
+ self.setex(name=key, time=expires_in_sec, value=pickle.dumps(val))
else:
self.set(key, pickle.dumps(val))
diff --git a/frappe/www/desk.py b/frappe/www/desk.py
index 689bf725f0..6cb7c8a077 100644
--- a/frappe/www/desk.py
+++ b/frappe/www/desk.py
@@ -12,8 +12,9 @@ from frappe import _
import frappe.sessions
def get_context(context):
- if (frappe.session.user == "Guest" or
- frappe.db.get_value("User", frappe.session.user, "user_type")=="Website User"):
+ if frappe.session.user == "Guest":
+ frappe.throw(_("Log in to access this page."), frappe.PermissionError)
+ elif frappe.db.get_value("User", frappe.session.user, "user_type") == "Website User":
frappe.throw(_("You are not permitted to access this page."), frappe.PermissionError)
hooks = frappe.get_hooks()