Merge branch 'develop' into feat-email-improvements
This commit is contained in:
commit
ea780bda26
73 changed files with 963 additions and 320 deletions
|
|
@ -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,
|
||||
|
|
|
|||
17
CODEOWNERS
Normal file
17
CODEOWNERS
Normal file
|
|
@ -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
|
||||
|
|
@ -81,10 +81,8 @@ def handle():
|
|||
frappe.local.response.update({"data": doc})
|
||||
|
||||
if frappe.local.request.method=="PUT":
|
||||
if frappe.local.form_dict.data is None:
|
||||
data = json.loads(frappe.safe_decode(frappe.local.request.get_data()))
|
||||
else:
|
||||
data = json.loads(frappe.local.form_dict.data)
|
||||
data = get_request_form_data()
|
||||
|
||||
doc = frappe.get_doc(doctype, name)
|
||||
|
||||
if "flags" in data:
|
||||
|
|
@ -123,10 +121,7 @@ def handle():
|
|||
})
|
||||
|
||||
if frappe.local.request.method == "POST":
|
||||
if frappe.local.form_dict.data is None:
|
||||
data = json.loads(frappe.safe_decode(frappe.local.request.get_data()))
|
||||
else:
|
||||
data = json.loads(frappe.local.form_dict.data)
|
||||
data = get_request_form_data()
|
||||
data.update({
|
||||
"doctype": doctype
|
||||
})
|
||||
|
|
@ -142,6 +137,13 @@ def handle():
|
|||
|
||||
return build_response("json")
|
||||
|
||||
def get_request_form_data():
|
||||
if frappe.local.form_dict.data is None:
|
||||
data = frappe.safe_decode(frappe.local.request.get_data())
|
||||
else:
|
||||
data = frappe.local.form_dict.data
|
||||
|
||||
return frappe.parse_json(data)
|
||||
|
||||
def validate_oauth():
|
||||
""" authentication using oauth """
|
||||
|
|
|
|||
|
|
@ -69,23 +69,25 @@ class Contact(Document):
|
|||
return True
|
||||
|
||||
def add_email(self, email_id, is_primary=0, autosave=False):
|
||||
self.append("email_ids", {
|
||||
"email_id": email_id,
|
||||
"is_primary": is_primary
|
||||
})
|
||||
if not frappe.db.exists("Contact Email", {"email_id": email_id, "parent": self.name}):
|
||||
self.append("email_ids", {
|
||||
"email_id": email_id,
|
||||
"is_primary": is_primary
|
||||
})
|
||||
|
||||
if autosave:
|
||||
self.save(ignore_permissions=True)
|
||||
if autosave:
|
||||
self.save(ignore_permissions=True)
|
||||
|
||||
def add_phone(self, phone, is_primary_phone=0, is_primary_mobile_no=0, autosave=False):
|
||||
self.append("phone_nos", {
|
||||
"phone": phone,
|
||||
"is_primary_phone": is_primary_phone,
|
||||
"is_primary_mobile_no": is_primary_mobile_no
|
||||
})
|
||||
if not frappe.db.exists("Contact Phone", {"phone": phone, "parent": self.name}):
|
||||
self.append("phone_nos", {
|
||||
"phone": phone,
|
||||
"is_primary_phone": is_primary_phone,
|
||||
"is_primary_mobile_no": is_primary_mobile_no
|
||||
})
|
||||
|
||||
if autosave:
|
||||
self.save(ignore_permissions=True)
|
||||
if autosave:
|
||||
self.save(ignore_permissions=True)
|
||||
|
||||
def set_primary_email(self):
|
||||
if not self.email_ids:
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"creation": "2013-01-29 10:47:14",
|
||||
"description": "Keeps track of all communications",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"email_append_to": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"subject",
|
||||
|
|
@ -384,7 +386,8 @@
|
|||
],
|
||||
"icon": "fa fa-comment",
|
||||
"idx": 1,
|
||||
"modified": "2019-10-09 14:22:27.664645",
|
||||
"links": [],
|
||||
"modified": "2019-12-27 14:44:04.880373",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Communication",
|
||||
|
|
@ -440,8 +443,10 @@
|
|||
}
|
||||
],
|
||||
"search_fields": "subject",
|
||||
"sender_field": "sender",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"subject_field": "subject",
|
||||
"title_field": "subject",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ frappe.ui.form.on('DocType', {
|
|||
frm.events.autoname(frm);
|
||||
},
|
||||
|
||||
autoname(frm) {
|
||||
autoname: function(frm) {
|
||||
frm.set_df_property('fields', 'reqd', frm.doc.autoname !== 'Prompt');
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@
|
|||
"color",
|
||||
"show_preview_popup",
|
||||
"show_name_in_global_search",
|
||||
"email_settings_sb",
|
||||
"email_append_to",
|
||||
"sender_field",
|
||||
"subject_field",
|
||||
"sb2",
|
||||
"permissions",
|
||||
"restrict_to_domain",
|
||||
|
|
@ -488,11 +492,37 @@
|
|||
"fieldtype": "Table",
|
||||
"label": "Links",
|
||||
"options": "DocType Link"
|
||||
},
|
||||
{
|
||||
"depends_on": "email_append_to",
|
||||
"fieldname": "subject_field",
|
||||
"fieldtype": "Data",
|
||||
"label": "Subject Field"
|
||||
},
|
||||
{
|
||||
"depends_on": "email_append_to",
|
||||
"fieldname": "sender_field",
|
||||
"fieldtype": "Data",
|
||||
"label": "Sender Field",
|
||||
"mandatory_depends_on": "email_append_to"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "email_append_to",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow document creation via Email"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "email_settings_sb",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Email Settings"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-bolt",
|
||||
"idx": 6,
|
||||
"modified": "2019-11-25 17:24:03.690192",
|
||||
"links": [],
|
||||
"modified": "2020-03-27 14:51:44.581128",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocType",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -94,10 +94,11 @@ class DocType(Document):
|
|||
if not self.is_new():
|
||||
self.setup_fields_to_fetch()
|
||||
|
||||
check_email_append_to(self)
|
||||
|
||||
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]:
|
||||
|
|
@ -108,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:
|
||||
|
|
@ -124,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:
|
||||
|
|
@ -169,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:
|
||||
|
|
@ -194,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:
|
||||
|
|
@ -204,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',
|
||||
|
|
@ -234,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
|
||||
|
|
@ -271,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()
|
||||
|
|
@ -325,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
|
||||
|
|
@ -343,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`'''
|
||||
|
|
@ -363,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":
|
||||
|
|
@ -386,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."""
|
||||
|
|
@ -397,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)
|
||||
|
|
@ -414,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)))
|
||||
|
|
@ -440,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"})
|
||||
|
|
@ -460,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):
|
||||
|
|
@ -505,7 +487,6 @@ class DocType(Document):
|
|||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
@staticmethod
|
||||
def prepare_for_import(docdict):
|
||||
# set order of fields from field_order
|
||||
|
|
@ -528,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)
|
||||
|
|
@ -557,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:
|
||||
|
|
@ -573,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:
|
||||
|
|
@ -642,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
|
||||
|
|
@ -669,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
|
||||
|
|
@ -693,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:
|
||||
|
|
@ -734,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))
|
||||
|
|
@ -763,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'
|
||||
|
|
@ -772,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
|
||||
|
|
@ -799,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):
|
||||
|
|
@ -814,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:
|
||||
|
|
@ -831,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"):
|
||||
|
|
@ -858,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:
|
||||
|
|
@ -870,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
|
||||
|
|
@ -878,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
|
||||
|
|
@ -890,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:
|
||||
|
|
@ -903,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"]
|
||||
|
|
@ -913,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
|
||||
|
|
@ -926,7 +877,6 @@ def validate_fields(meta):
|
|||
frappe.throw(_('DocType <b>{0}</b> provided for the field <b>{1}</b> must have atleast one Link field')
|
||||
.format(doctype, docfield.fieldname), frappe.ValidationError)
|
||||
|
||||
|
||||
def scrub_options_in_select(field):
|
||||
"""Strip options for whitespaces"""
|
||||
|
||||
|
|
@ -938,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) + "<br>" * 2 + _("Only Options allowed for Data field are:") + "<br>"
|
||||
df_options_str = "<ul><li>" + "</li><li>".join([_(x) for x in data_field_options]) + "</ul>"
|
||||
|
||||
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]
|
||||
|
||||
|
|
@ -973,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)
|
||||
|
|
@ -982,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)
|
||||
|
|
@ -994,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)
|
||||
|
|
@ -1009,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:
|
||||
|
|
@ -1103,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:
|
||||
|
|
@ -1134,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))]
|
||||
|
|
@ -1142,6 +1097,38 @@ 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')
|
||||
|
||||
def check_email_append_to(doc):
|
||||
if not hasattr(doc, "email_append_to") or not doc.email_append_to:
|
||||
return
|
||||
|
||||
# Subject Field
|
||||
doc.subject_field = doc.subject_field.strip() if doc.subject_field else None
|
||||
subject_field = get_field(doc, doc.subject_field)
|
||||
|
||||
if doc.subject_field and not subject_field:
|
||||
frappe.throw(_("Select a valid Subject field for creating documents from Email"))
|
||||
|
||||
if subject_field and subject_field.fieldtype not in ["Data", "Text", "Long Text", "Small Text", "Text Editor"]:
|
||||
frappe.throw(_("Subject Field type should be Data, Text, Long Text, Small Text, Text Editor"))
|
||||
|
||||
# Sender Field is mandatory
|
||||
doc.sender_field = doc.sender_field.strip() if doc.sender_field else None
|
||||
sender_field = get_field(doc, doc.sender_field)
|
||||
|
||||
if doc.sender_field and not sender_field:
|
||||
frappe.throw(_("Select a valid Sender Field for creating documents from Email"))
|
||||
|
||||
if not sender_field.options == "Email":
|
||||
frappe.throw(_("Sender Field should have Email in options"))
|
||||
|
||||
|
||||
def get_field(doc, fieldname):
|
||||
if not (doc or fieldname):
|
||||
return
|
||||
|
||||
for field in doc.fields:
|
||||
if field.fieldname == fieldname:
|
||||
return field
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
frappe.db.commit()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Role Profile', {
|
||||
setup: function(frm) {
|
||||
refresh: function(frm) {
|
||||
if(has_common(frappe.user_roles, ["Administrator", "System Manager"])) {
|
||||
if(!frm.roles_editor) {
|
||||
var role_area = $('<div style="min-height: 300px">')
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@ frappe.PermissionEngine = Class.extend({
|
|||
|
||||
me.rights.forEach(r => {
|
||||
if (!d.is_submittable && ['submit', 'cancel', 'amend'].includes(r)) return;
|
||||
if (d.in_create && ['create', 'write', 'delete'].includes(r)) return;
|
||||
me.add_check(perm_container, d, r);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ def get_permissions(doctype=None, role=None):
|
|||
meta = frappe.get_meta(d.parent)
|
||||
if meta:
|
||||
d.is_submittable = meta.is_submittable
|
||||
d.in_create = meta.in_create
|
||||
|
||||
return out
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -143,8 +143,7 @@ frappe.ui.form.on("Customize Form", {
|
|||
}, 1000);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Customize Form Field", {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "DL.####",
|
||||
"creation": "2013-01-29 17:55:08",
|
||||
"doctype": "DocType",
|
||||
|
|
@ -28,6 +29,10 @@
|
|||
"sort_field",
|
||||
"column_break_10",
|
||||
"sort_order",
|
||||
"section_break_23",
|
||||
"email_append_to",
|
||||
"sender_field",
|
||||
"subject_field",
|
||||
"fields_section_break",
|
||||
"fields"
|
||||
],
|
||||
|
|
@ -174,13 +179,38 @@
|
|||
"fieldname": "allow_import",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Import (via Data Import Tool)"
|
||||
},
|
||||
{
|
||||
"depends_on": "email_append_to",
|
||||
"fieldname": "subject_field",
|
||||
"fieldtype": "Data",
|
||||
"label": "Subject Field"
|
||||
},
|
||||
{
|
||||
"depends_on": "email_append_to",
|
||||
"fieldname": "sender_field",
|
||||
"fieldtype": "Data",
|
||||
"label": "Sender Field",
|
||||
"mandatory_depends_on": "email_append_to"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "email_append_to",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow document creation via Email"
|
||||
},
|
||||
{
|
||||
"depends_on": "doc_type",
|
||||
"fieldname": "section_break_23",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"icon": "fa fa-glass",
|
||||
"idx": 1,
|
||||
"issingle": 1,
|
||||
"modified": "2019-10-08 11:16:36.698006",
|
||||
"links": [],
|
||||
"modified": "2020-03-27 15:06:35.443861",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from frappe import _
|
|||
from frappe.utils import cint
|
||||
from frappe.model.document import Document
|
||||
from frappe.model import no_value_fields, core_doctypes_list
|
||||
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype
|
||||
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype, check_email_append_to
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
from frappe.model.docfield import supports_translation
|
||||
|
||||
|
|
@ -31,7 +31,10 @@ doctype_properties = {
|
|||
'track_changes': 'Check',
|
||||
'track_views': 'Check',
|
||||
'allow_auto_repeat': 'Check',
|
||||
'allow_import': 'Check'
|
||||
'allow_import': 'Check',
|
||||
'email_append_to': 'Check',
|
||||
'subject_field': 'Data',
|
||||
'sender_field': 'Data'
|
||||
}
|
||||
|
||||
docfield_properties = {
|
||||
|
|
@ -170,6 +173,7 @@ class CustomizeForm(Document):
|
|||
self.update_custom_fields()
|
||||
self.set_name_translation()
|
||||
validate_fields_for_doctype(self.doc_type)
|
||||
check_email_append_to(self)
|
||||
|
||||
if self.flags.update_db:
|
||||
frappe.db.updatedb(self.doc_type)
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class TestCustomizeForm(unittest.TestCase):
|
|||
|
||||
d = self.get_customize_form("Event")
|
||||
self.assertEquals(d.doc_type, "Event")
|
||||
self.assertEquals(len(d.get("fields")), 35)
|
||||
self.assertEquals(len(d.get("fields")), 36)
|
||||
|
||||
d = self.get_customize_form("Event")
|
||||
self.assertEquals(d.doc_type, "Event")
|
||||
|
|
|
|||
|
|
@ -217,6 +217,9 @@ CREATE TABLE `tabDocType` (
|
|||
`allow_guest_to_view` int(1) NOT NULL DEFAULT 0,
|
||||
`route` varchar(255) DEFAULT NULL,
|
||||
`is_published_field` varchar(255) DEFAULT NULL,
|
||||
`email_append_to` int(1) NOT NULL DEFAULT 0,
|
||||
`subject_field` varchar(255) DEFAULT NULL,
|
||||
`sender_field` varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY (`name`),
|
||||
KEY `parent` (`parent`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
|
|
|||
|
|
@ -222,6 +222,9 @@ CREATE TABLE "tabDocType" (
|
|||
"allow_guest_to_view" smallint NOT NULL DEFAULT 0,
|
||||
"route" varchar(255) DEFAULT NULL,
|
||||
"is_published_field" varchar(255) DEFAULT NULL,
|
||||
"email_append_to" smallint NOT NULL DEFAULT 0,
|
||||
"subject_field" varchar(255) DEFAULT NULL,
|
||||
"sender_field" varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY ("name")
|
||||
) ;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
0
frappe/desk/doctype/dashboard_settings/__init__.py
Normal file
0
frappe/desk/doctype/dashboard_settings/__init__.py
Normal file
|
|
@ -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) {
|
||||
|
||||
// }
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
43
frappe/desk/doctype/dashboard_settings/dashboard_settings.py
Normal file
43
frappe/desk/doctype/dashboard_settings/dashboard_settings.py
Normal file
|
|
@ -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))
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"autoname": "EV.#####",
|
||||
"creation": "2013-06-10 13:17:47",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"email_append_to": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"details",
|
||||
|
|
@ -17,6 +19,7 @@
|
|||
"starts_on",
|
||||
"ends_on",
|
||||
"status",
|
||||
"sender",
|
||||
"all_day",
|
||||
"sync_with_google_calendar",
|
||||
"sb_00",
|
||||
|
|
@ -262,11 +265,19 @@
|
|||
"fieldtype": "Check",
|
||||
"label": "Pulled from Google Calendar",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sender",
|
||||
"fieldtype": "Data",
|
||||
"label": "Sender",
|
||||
"options": "Email",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-calendar",
|
||||
"idx": 1,
|
||||
"modified": "2019-08-08 16:01:19.489396",
|
||||
"links": [],
|
||||
"modified": "2020-01-14 21:47:15.825287",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Event",
|
||||
|
|
@ -297,8 +308,10 @@
|
|||
}
|
||||
],
|
||||
"read_only": 1,
|
||||
"sender_field": "sender",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"subject_field": "subject",
|
||||
"title_field": "subject",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"creation": "2012-07-03 13:30:35",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"email_append_to": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"description_and_status",
|
||||
|
|
@ -142,7 +144,8 @@
|
|||
"fieldname": "sender",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Sender"
|
||||
"label": "Sender",
|
||||
"options": "Email"
|
||||
},
|
||||
{
|
||||
"fieldname": "assignment_rule",
|
||||
|
|
@ -154,7 +157,8 @@
|
|||
],
|
||||
"icon": "fa fa-check",
|
||||
"idx": 2,
|
||||
"modified": "2019-09-10 14:34:59.161750",
|
||||
"links": [],
|
||||
"modified": "2020-01-14 17:04:36.971002",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "ToDo",
|
||||
|
|
@ -185,9 +189,11 @@
|
|||
],
|
||||
"quick_entry": 1,
|
||||
"search_fields": "description, reference_type, reference_name",
|
||||
"sender_field": "sender",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"subject_field": "description",
|
||||
"title_field": "description",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -8,8 +8,6 @@ import json
|
|||
from frappe.model.document import Document
|
||||
from frappe.utils import get_fullname
|
||||
|
||||
subject_field = "description"
|
||||
sender_field = "sender"
|
||||
exclude_from_linked_with = True
|
||||
|
||||
class ToDo(Document):
|
||||
|
|
|
|||
|
|
@ -56,18 +56,20 @@ def validate_link():
|
|||
frappe.response['valid_value'] = valid_value
|
||||
frappe.response['message'] = 'Ok'
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_comment(reference_doctype, reference_name, content, comment_email):
|
||||
def add_comment(reference_doctype, reference_name, content, comment_email, comment_by):
|
||||
"""allow any logged user to post a comment"""
|
||||
doc = frappe.get_doc(dict(
|
||||
doctype = 'Comment',
|
||||
reference_doctype = reference_doctype,
|
||||
reference_name = reference_name,
|
||||
comment_email = comment_email,
|
||||
comment_type = 'Comment'
|
||||
doctype='Comment',
|
||||
reference_doctype=reference_doctype,
|
||||
reference_name=reference_name,
|
||||
comment_email=comment_email,
|
||||
comment_type='Comment',
|
||||
comment_by=comment_by
|
||||
))
|
||||
doc.content = extract_images_from_html(doc, content)
|
||||
doc.insert(ignore_permissions = True)
|
||||
doc.insert(ignore_permissions=True)
|
||||
|
||||
follow_document(doc.reference_doctype, doc.reference_name, frappe.session.user)
|
||||
return doc.as_dict()
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -452,16 +452,15 @@ class EmailAccount(Document):
|
|||
def set_sender_field_and_subject_field(self):
|
||||
'''Identify the sender and subject fields from the `append_to` DocType'''
|
||||
# set subject_field and sender_field
|
||||
meta_module = frappe.get_meta_module(self.append_to)
|
||||
meta = frappe.get_meta(self.append_to)
|
||||
self.subject_field = None
|
||||
self.sender_field = None
|
||||
|
||||
self.subject_field = getattr(meta_module, "subject_field", "subject")
|
||||
if not meta.get_field(self.subject_field):
|
||||
self.subject_field = None
|
||||
if hasattr(meta, "subject_field"):
|
||||
self.subject_field = meta.subject_field
|
||||
|
||||
self.sender_field = getattr(meta_module, "sender_field", "sender")
|
||||
if not meta.get_field(self.sender_field):
|
||||
self.sender_field = None
|
||||
if hasattr(meta, "sender_field"):
|
||||
self.sender_field = meta.sender_field
|
||||
|
||||
def find_parent_based_on_subject_and_sender(self, communication, email):
|
||||
'''Find parent document based on subject and sender match'''
|
||||
|
|
@ -675,8 +674,21 @@ class EmailAccount(Document):
|
|||
|
||||
@frappe.whitelist()
|
||||
def get_append_to(doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None):
|
||||
if not txt: txt = ""
|
||||
return [[d] for d in frappe.get_hooks("email_append_to") if txt in d]
|
||||
txt = txt if txt else ""
|
||||
email_append_to_list = []
|
||||
|
||||
# Set Email Append To DocTypes via DocType
|
||||
filters = {"istable": 0, "issingle": 0, "email_append_to": 1}
|
||||
for dt in frappe.get_all("DocType", filters=filters, fields=["name", "email_append_to"]):
|
||||
email_append_to_list.append(dt.name)
|
||||
|
||||
# Set Email Append To DocTypes set via Customize Form
|
||||
for dt in frappe.get_list("Property Setter", filters={"property": "email_append_to", "value": 1}, fields=["doc_type"]):
|
||||
email_append_to_list.append(dt.doc_type)
|
||||
|
||||
email_append_to = [[d] for d in set(email_append_to_list) if txt in d]
|
||||
|
||||
return email_append_to
|
||||
|
||||
def test_internet(host="8.8.8.8", port=53, timeout=3):
|
||||
"""Returns True if internet is connected
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ def load_doctype_from_file(doctype):
|
|||
class Meta(Document):
|
||||
_metaclass = True
|
||||
default_fields = list(default_fields)[1:]
|
||||
special_doctypes = ("DocField", "DocPerm", "Role", "DocType", "Module Def")
|
||||
special_doctypes = ("DocField", "DocPerm", "Role", "DocType", "Module Def", 'DocType Action', 'DocType Link')
|
||||
|
||||
def __init__(self, doctype):
|
||||
self._fields = {}
|
||||
|
|
@ -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"})
|
||||
|
|
@ -165,7 +168,8 @@ class Meta(Document):
|
|||
|
||||
def get_valid_columns(self):
|
||||
if not hasattr(self, "_valid_columns"):
|
||||
if self.name in ("DocType", "DocField", "DocPerm", 'DocType Action', 'DocType Link'):
|
||||
table_exists = frappe.db.table_exists(self.name)
|
||||
if self.name in self.special_doctypes and table_exists:
|
||||
self._valid_columns = get_table_columns(self.name)
|
||||
else:
|
||||
self._valid_columns = self.default_fields + \
|
||||
|
|
|
|||
|
|
@ -45,20 +45,29 @@ def get_transitions(doc, workflow = None, raise_exception=False):
|
|||
transitions = []
|
||||
for transition in workflow.transitions:
|
||||
if transition.state == current_state and transition.allowed in roles:
|
||||
if transition.condition:
|
||||
# if condition, evaluate
|
||||
# access to frappe.db.get_value and frappe.db.get_list
|
||||
success = frappe.safe_eval(transition.condition,
|
||||
dict(frappe = frappe._dict(
|
||||
db = frappe._dict(get_value = frappe.db.get_value, get_list=frappe.db.get_list),
|
||||
session = frappe.session
|
||||
)),
|
||||
dict(doc = doc))
|
||||
if not success:
|
||||
continue
|
||||
if not is_transition_condition_satisfied(transition, doc):
|
||||
continue
|
||||
transitions.append(transition.as_dict())
|
||||
return transitions
|
||||
|
||||
def get_workflow_safe_globals():
|
||||
# access to frappe.db.get_value and frappe.db.get_list
|
||||
return dict(
|
||||
frappe=frappe._dict(
|
||||
db=frappe._dict(
|
||||
get_value=frappe.db.get_value,
|
||||
get_list=frappe.db.get_list
|
||||
),
|
||||
session=frappe.session
|
||||
)
|
||||
)
|
||||
|
||||
def is_transition_condition_satisfied(transition, doc):
|
||||
if not transition.condition:
|
||||
return True
|
||||
else:
|
||||
return frappe.safe_eval(transition.condition, get_workflow_safe_globals(), dict(doc=doc.as_dict()))
|
||||
|
||||
@frappe.whitelist()
|
||||
def apply_workflow(doc, action):
|
||||
'''Allow workflow action on the current doc'''
|
||||
|
|
@ -185,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
|
||||
|
|
@ -206,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))
|
||||
|
|
@ -219,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):
|
||||
|
|
|
|||
|
|
@ -270,3 +270,4 @@ execute:frappe.delete_doc_if_exists('DocType', 'GSuite Settings')
|
|||
execute:frappe.delete_doc_if_exists('DocType', 'GSuite Templates')
|
||||
execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Account')
|
||||
execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Settings')
|
||||
frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats
|
||||
|
|
|
|||
|
|
@ -5,7 +5,11 @@ def execute():
|
|||
SELECT
|
||||
`name`, `email_id`, `phone`, `mobile_no`, `modified_by`, `creation`, `modified`
|
||||
FROM `tabContact`
|
||||
where not exists (select * from `tabContact Email`
|
||||
where `tabContact Email`.parent=`tabContact`.name
|
||||
and `tabContact Email`.email_id=`tabContact`.email_id)
|
||||
""", as_dict=True)
|
||||
|
||||
frappe.reload_doc("contacts", "doctype", "contact_email")
|
||||
frappe.reload_doc("contacts", "doctype", "contact_phone")
|
||||
frappe.reload_doc("contacts", "doctype", "contact")
|
||||
|
|
@ -15,7 +19,6 @@ def execute():
|
|||
for count, contact_detail in enumerate(contact_details):
|
||||
phone_counter = 1
|
||||
is_primary = 1
|
||||
|
||||
if contact_detail.email_id:
|
||||
email_values.append((
|
||||
1,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.db.sql("""
|
||||
UPDATE
|
||||
`tabPrint Format`
|
||||
SET
|
||||
`tabPrint Format`.`parent`='',
|
||||
`tabPrint Format`.`parenttype`='',
|
||||
`tabPrint Format`.parentfield=''
|
||||
WHERE
|
||||
`tabPrint Format`.parent != ''
|
||||
OR `tabPrint Format`.parenttype != ''
|
||||
""")
|
||||
|
|
@ -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'''
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ frappe.ui.form.ControlMarkdownEditor = frappe.ui.form.ControlCode.extend({
|
|||
this.ace_editor_target.wrap(`<div class="${this.editor_class}-container">`);
|
||||
this.markdown_container = this.$input_wrapper.find(`.${this.editor_class}-container`);
|
||||
|
||||
this.editor.getSession().setUseWrapMode(true);
|
||||
|
||||
this.showing_preview = false;
|
||||
this.preview_toggle_btn = $(`<button class="btn btn-default btn-xs ${this.editor_class}-toggle">${__('Preview')}</button>`)
|
||||
.click(e => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -703,7 +702,8 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
reference_doctype: this.frm.doctype,
|
||||
reference_name: this.frm.docname,
|
||||
content: comment,
|
||||
comment_email: frappe.session.user
|
||||
comment_email: frappe.session.user,
|
||||
comment_by: frappe.session.user_fullname
|
||||
},
|
||||
btn: btn,
|
||||
callback: function(r) {
|
||||
|
|
|
|||
|
|
@ -161,6 +161,10 @@ $.extend(frappe.model, {
|
|||
user_default = frappe.boot.user.last_selected_values[df.options];
|
||||
}
|
||||
|
||||
if (!user_default && default_doc) {
|
||||
user_default = default_doc;
|
||||
}
|
||||
|
||||
var is_allowed_user_default = user_default &&
|
||||
(!has_user_permissions || allowed_records.includes(user_default));
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -28,4 +28,12 @@
|
|||
}
|
||||
.page-card p {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 == [])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
return html
|
||||
|
|
|
|||
|
|
@ -151,6 +151,10 @@ app_license = "{app_license}"
|
|||
# web_include_css = "/assets/{app_name}/css/{app_name}.css"
|
||||
# web_include_js = "/assets/{app_name}/js/{app_name}.js"
|
||||
|
||||
# include js, css files in header of web form
|
||||
# webform_include_js = {"doctype": "public/js/doctype.js"}
|
||||
# webform_include_css = {"doctype": "public/css/doctype.css"}
|
||||
|
||||
# include js in page
|
||||
# page_js = {{"page" : "public/js/file.js"}}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -2,18 +2,23 @@
|
|||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe, json, os
|
||||
from frappe.website.website_generator import WebsiteGenerator
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from six import iteritems
|
||||
from six.moves.urllib.parse import urlencode
|
||||
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe.core.doctype.file.file import get_max_file_size, remove_file_by_url
|
||||
from frappe.custom.doctype.customize_form.customize_form import docfield_properties
|
||||
from frappe.desk.form.meta import get_code_files_via_hooks
|
||||
from frappe.integrations.utils import get_payment_gateway_controller
|
||||
from frappe.modules.utils import export_module_json, get_doc_module
|
||||
from frappe.utils import cstr
|
||||
from frappe.website.utils import get_comment_list
|
||||
from frappe.custom.doctype.customize_form.customize_form import docfield_properties
|
||||
from frappe.core.doctype.file.file import get_max_file_size
|
||||
from frappe.core.doctype.file.file import remove_file_by_url
|
||||
from frappe.modules.utils import export_module_json, get_doc_module
|
||||
from six.moves.urllib.parse import urlencode
|
||||
from frappe.integrations.utils import get_payment_gateway_controller
|
||||
from six import iteritems
|
||||
from frappe.website.website_generator import WebsiteGenerator
|
||||
|
||||
|
||||
class WebForm(WebsiteGenerator):
|
||||
|
|
@ -237,11 +242,23 @@ def get_context(context):
|
|||
|
||||
js_path = os.path.join(os.path.dirname(self.web_form_module.__file__), scrub(self.name) + '.js')
|
||||
if os.path.exists(js_path):
|
||||
context.script = frappe.render_template(open(js_path, 'r').read(), context)
|
||||
script = frappe.render_template(open(js_path, 'r').read(), context)
|
||||
|
||||
for path in get_code_files_via_hooks("webform_include_js", context.doc_type):
|
||||
custom_js = frappe.render_template(open(path, 'r').read(), context)
|
||||
script = "\n\n".join([script, custom_js])
|
||||
|
||||
context.script = script
|
||||
|
||||
css_path = os.path.join(os.path.dirname(self.web_form_module.__file__), scrub(self.name) + '.css')
|
||||
if os.path.exists(css_path):
|
||||
context.style = open(css_path, 'r').read()
|
||||
style = open(css_path, 'r').read()
|
||||
|
||||
for path in get_code_files_via_hooks("webform_include_css", context.doc_type):
|
||||
custom_css = open(path, 'r').read()
|
||||
style = "\n\n".join([style, custom_css])
|
||||
|
||||
context.style = style
|
||||
|
||||
def get_layout(self):
|
||||
layout = []
|
||||
|
|
@ -589,4 +606,3 @@ def get_link_options(web_form_name, doctype, allow_read_on_all_link_options=Fals
|
|||
|
||||
else:
|
||||
raise frappe.PermissionError('Not Allowed, {0}'.format(doctype))
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,15 @@
|
|||
|
||||
from __future__ import unicode_literals
|
||||
import functools
|
||||
import frappe, re, os
|
||||
import re
|
||||
import os
|
||||
import frappe
|
||||
|
||||
from six import iteritems
|
||||
from past.builtins import cmp
|
||||
from frappe.utils import markdown
|
||||
|
||||
|
||||
def delete_page_cache(path):
|
||||
cache = frappe.cache()
|
||||
cache.delete_value('full_index')
|
||||
|
|
@ -20,7 +24,7 @@ def delete_page_cache(path):
|
|||
cache.delete_key(name)
|
||||
|
||||
def find_first_image(html):
|
||||
m = re.finditer("""<img[^>]*src\s?=\s?['"]([^'"]*)['"]""", html)
|
||||
m = re.finditer(r"""<img[^>]*src\s?=\s?['"]([^'"]*)['"]""", html)
|
||||
try:
|
||||
return next(m).groups()[0]
|
||||
except StopIteration:
|
||||
|
|
@ -33,16 +37,34 @@ def can_cache(no_cache=False):
|
|||
return False
|
||||
return not no_cache
|
||||
|
||||
|
||||
def get_comment_list(doctype, name):
|
||||
return frappe.get_all('Comment',
|
||||
fields = ['name', 'creation', 'owner', 'comment_email', 'comment_by', 'content'],
|
||||
filters = dict(
|
||||
reference_doctype = doctype,
|
||||
reference_name = name,
|
||||
comment_type = 'Comment',
|
||||
published = 1
|
||||
),
|
||||
order_by = 'creation asc')
|
||||
comments = frappe.get_all('Comment',
|
||||
fields=['name', 'creation', 'owner',
|
||||
'comment_email', 'comment_by', 'content'],
|
||||
filters=dict(
|
||||
reference_doctype=doctype,
|
||||
reference_name=name,
|
||||
comment_type='Comment',
|
||||
),
|
||||
or_filters=[
|
||||
['owner', '=', frappe.session.user],
|
||||
['published', '=', 1]])
|
||||
|
||||
communications = frappe.get_all("Communication",
|
||||
fields=['name', 'creation', 'owner', 'owner as comment_email',
|
||||
'sender_full_name as comment_by', 'content', 'recipients'],
|
||||
filters=dict(
|
||||
reference_doctype=doctype,
|
||||
reference_name=name,
|
||||
),
|
||||
or_filters=[
|
||||
['recipients', 'like', '%{0}%'.format(frappe.session.user)],
|
||||
['cc', 'like', '%{0}%'.format(frappe.session.user)],
|
||||
['bcc', 'like', '%{0}%'.format(frappe.session.user)]])
|
||||
|
||||
return sorted((comments + communications), key=lambda comment: comment['creation'], reverse=True)
|
||||
|
||||
|
||||
def get_home_page():
|
||||
if frappe.local.flags.home_page:
|
||||
|
|
@ -92,7 +114,7 @@ def cleanup_page_name(title):
|
|||
return ''
|
||||
|
||||
name = title.lower()
|
||||
name = re.sub('[~!@#$%^&*+()<>,."\'\?]', '', name)
|
||||
name = re.sub(r'[~!@#$%^&*+()<>,."\'\?]', '', name)
|
||||
name = re.sub('[:/]', '-', name)
|
||||
|
||||
name = '-'.join(name.split())
|
||||
|
|
@ -194,7 +216,6 @@ def abs_url(path):
|
|||
|
||||
def get_toc(route, url_prefix=None, app=None):
|
||||
'''Insert full index (table of contents) for {index} tag'''
|
||||
from frappe.website.utils import get_full_index
|
||||
|
||||
full_index = get_full_index(app=app)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ from frappe.utils import get_url, get_datetime
|
|||
from frappe.desk.form.utils import get_pdf_link
|
||||
from frappe.utils.verified_command import get_signed_params, verify_request
|
||||
from frappe import _
|
||||
from frappe.model.workflow import apply_workflow, get_workflow_name, \
|
||||
has_approval_access, get_workflow_state_field, send_email_alert, get_workflow_field_value
|
||||
from frappe.model.workflow import apply_workflow, get_workflow_name, has_approval_access, \
|
||||
get_workflow_state_field, send_email_alert, get_workflow_field_value, is_transition_condition_satisfied
|
||||
from frappe.desk.notifications import clear_doctype_notifications
|
||||
from frappe.utils.user import get_users_with_role
|
||||
|
||||
|
|
@ -155,10 +155,10 @@ def get_next_possible_transitions(workflow_name, state, doc=None):
|
|||
for transition in transitions:
|
||||
is_next_state_optional = get_state_optional_field_value(workflow_name, transition.next_state)
|
||||
# skip transition if next state of the transition is optional
|
||||
if transition.condition and not frappe.safe_eval(transition.condition, None, {'doc': doc.as_dict()}):
|
||||
continue
|
||||
if is_next_state_optional:
|
||||
continue
|
||||
if not is_transition_condition_satisfied(transition, doc):
|
||||
continue
|
||||
transitions_to_return.append(transition)
|
||||
|
||||
return transitions_to_return
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ html, body {
|
|||
<span class='indicator {{ indicator_color or "blue" }}'>
|
||||
{{ title or _("Message") }}</span>
|
||||
</h5>
|
||||
<div class="page-card-body">
|
||||
<div class="page-card-body ellipsis">
|
||||
{% block message_body %}
|
||||
<p>{{ message or "" }}</p>
|
||||
{% if primary_action %}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue