Merge branch 'develop' into logout-message

This commit is contained in:
Suraj Shetty 2020-04-01 22:20:22 +05:30 committed by GitHub
commit 7ea493d9b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
73 changed files with 815 additions and 310 deletions

View file

@ -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,

View file

@ -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 """

View file

@ -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:

View file

@ -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",
@ -383,7 +385,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",
@ -430,8 +433,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

View file

@ -1,4 +1,4 @@
<div>
<a href={{{{ route }}}}>{{{{ title }}}}</a>
<a href="{{{{ doc.route }}}}">{{{{ doc.title or doc.name }}}}</a>
</div>
<!-- this is a sample default list template -->
<!-- this is a sample default list template -->

View file

@ -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');
}
})

View file

@ -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",

View file

@ -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
@ -24,6 +24,7 @@ from frappe.modules import make_boilerplate, get_doc_path
from frappe.database.schema import validate_column_name, validate_column_length
from frappe.model.docfield import supports_translation
from frappe.modules.import_file import get_file_path
from frappe.model.meta import Meta
class InvalidFieldNameError(frappe.ValidationError): pass
@ -93,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]:
@ -107,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:
@ -123,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:
@ -168,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:
@ -193,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:
@ -203,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',
@ -233,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
@ -270,12 +263,11 @@ 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()
try:
frappe.db.updatedb(self.name, self)
frappe.db.updatedb(self.name, Meta(self))
except Exception as e:
print("\n\nThere was an issue while migrating the DocType: {}\n".format(self.name))
raise e
@ -324,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
@ -342,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`'''
@ -362,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":
@ -385,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."""
@ -396,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)
@ -413,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)))
@ -439,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"})
@ -459,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):
@ -504,7 +487,6 @@ class DocType(Document):
except ValueError:
pass
@staticmethod
def prepare_for_import(docdict):
# set order of fields from field_order
@ -527,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)
@ -556,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:
@ -572,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:
@ -641,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
@ -668,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
@ -692,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:
@ -733,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))
@ -762,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'
@ -771,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
@ -798,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):
@ -813,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:
@ -830,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"):
@ -857,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:
@ -869,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
@ -877,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
@ -889,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:
@ -902,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"]
@ -912,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
@ -925,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"""
@ -937,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]
@ -972,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)
@ -981,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)
@ -993,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)
@ -1008,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:
@ -1102,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:
@ -1133,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))]
@ -1141,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

View file

@ -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()

View file

@ -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">')

View file

@ -37,6 +37,7 @@
"allow_login_using_user_name",
"allow_error_traceback",
"password_settings",
"logout_on_password_reset",
"force_user_to_reset_password",
"column_break_31",
"enable_password_policy",
@ -407,6 +408,12 @@
"fieldname": "dormant_days",
"fieldtype": "Int",
"label": "Run Jobs only Daily if Inactive For (Days)"
},
{
"default": "1",
"fieldname": "logout_on_password_reset",
"fieldtype": "Check",
"label": "Logout All Sessions on Password Reset"
}
],
"icon": "fa fa-cog",

View file

@ -224,3 +224,4 @@ class TestUser(unittest.TestCase):
def delete_contact(user):
frappe.db.sql("DELETE FROM `tabContact` WHERE `email_id`= %s", user)
frappe.db.sql("DELETE FROM `tabContact Email` WHERE `email_id`= %s", user)

View file

@ -1,4 +1,5 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"creation": "2014-03-11 14:55:00",
@ -178,7 +179,7 @@
{
"fieldname": "time_zone",
"fieldtype": "Select",
"label": "Timezone"
"label": "Time Zone"
},
{
"description": "Get your globally recognized avatar from Gravatar.com",
@ -302,7 +303,7 @@
"default": "0",
"fieldname": "logout_all_sessions",
"fieldtype": "Check",
"label": "Logout from all devices while changing Password"
"label": "Logout From All Devices After Changing Password"
},
{
"fieldname": "reset_password_key",
@ -338,7 +339,7 @@
"default": "0",
"fieldname": "document_follow_notify",
"fieldtype": "Check",
"label": "Send Notifications for documents followed by me"
"label": "Send Notifications For Documents Followed By Me"
},
{
"default": "Daily",
@ -359,7 +360,7 @@
"default": "1",
"fieldname": "thread_notify",
"fieldtype": "Check",
"label": "Send Notifications for Email threads"
"label": "Send Notifications For Email Threads"
},
{
"default": "0",
@ -496,7 +497,7 @@
"description": "If enabled, user can login from any IP Address using Two Factor Auth, this can also be set for all users in System Settings",
"fieldname": "bypass_restrict_ip_check_if_2fa_enabled",
"fieldtype": "Check",
"label": "Bypass restricted IP Address check If Two Factor Auth Enabled"
"label": "Bypass Restricted IP Address Check If Two Factor Auth Enabled"
},
{
"fieldname": "column_break1",
@ -585,8 +586,9 @@
"icon": "fa fa-user",
"idx": 413,
"image_field": "user_image",
"links": [],
"max_attachments": 5,
"modified": "2019-10-22 14:16:34.810223",
"modified": "2020-03-23 22:59:26.154985",
"modified_by": "Administrator",
"module": "Core",
"name": "User",

View file

@ -555,7 +555,8 @@ def update_password(new_password, logout_all_sessions=0, key=None, old_password=
else:
user = res['user']
_update_password(user, new_password, logout_all_sessions=int(logout_all_sessions))
logout_all_sessions = cint(logout_all_sessions) or frappe.db.get_single_value("System Settings", "logout_on_password_reset")
_update_password(user, new_password, logout_all_sessions=cint(logout_all_sessions))
user_doc, redirect_url = reset_user_data(user)

View file

@ -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,9 +76,11 @@ class Dashboard {
}
refresh() {
this.get_dashboard_doc().then((doc) => {
this.dashboard_doc = doc;
this.charts = this.dashboard_doc.charts
this.get_permitted_dashboard_charts().then(charts => {
if (!charts.length) {
frappe.msgprint(__('No Permitted Charts on this Dashboard'), __('No Permitted Charts'))
}
this.charts = charts
.map(chart => {
return {
chart_name: chart.chart,
@ -98,8 +100,14 @@ class Dashboard {
});
}
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() {

View file

@ -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",

View file

@ -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);
});

View file

@ -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

View file

@ -85,6 +85,10 @@ frappe.ui.form.on("Customize Form", {
if(frm.doc.doc_type) {
frappe.customize_form.set_primary_action(frm);
frm.add_custom_button(__('Go to {0} List', [frm.doc.doc_type]), function() {
frappe.set_route('List', frm.doc.doc_type);
});
frm.add_custom_button(__('Refresh Form'), function() {
frm.script_manager.trigger("doc_type");
}, "fa fa-refresh", "btn-default");
@ -139,8 +143,7 @@ frappe.ui.form.on("Customize Form", {
}, 1000);
}
},
}
});
frappe.ui.form.on("Customize Form Field", {

View file

@ -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",

View file

@ -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 = {
@ -166,11 +169,11 @@ class CustomizeForm(Document):
self.flags.update_db = False
self.flags.rebuild_doctype_for_global_search = False
self.set_property_setters()
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)
@ -362,13 +365,49 @@ class CustomizeForm(Document):
def validate_fieldtype_change(self, df, old_value, new_value):
allowed = False
self.check_length_for_fieldtypes = []
for allowed_changes in allowed_fieldtype_change:
if (old_value in allowed_changes and new_value in allowed_changes):
allowed = True
if frappe.db.type_map.get(old_value)[1] > frappe.db.type_map.get(new_value)[1]:
self.check_length_for_fieldtypes.append({'df': df, 'old_value': old_value})
self.validate_fieldtype_length()
else:
self.flags.update_db = True
break
if not allowed:
frappe.throw(_("Fieldtype cannot be changed from {0} to {1} in row {2}").format(old_value, new_value, df.idx))
def validate_fieldtype_length(self):
for field in self.check_length_for_fieldtypes:
df = field.get('df')
max_length = frappe.db.type_map.get(df.fieldtype)[1]
fieldname = df.fieldname
docs = frappe.db.sql('''
SELECT name, {fieldname}, LENGTH({fieldname}) AS len
FROM `tab{doctype}`
WHERE LENGTH({fieldname}) > {max_length}
'''.format(
fieldname=fieldname,
doctype=self.doc_type,
max_length=max_length
), as_dict=True)
links = []
label = df.label
for doc in docs:
links.append(frappe.utils.get_link_to_form(self.doc_type, doc.name))
links_str = ', '.join(links)
if docs:
frappe.throw(_('Value for field {0} is too long in {1}. Length should be lesser than {2} characters')
.format(
frappe.bold(label),
links_str,
frappe.bold(max_length)
), title=_('Data Too Long'), is_minimizable=len(docs) > 1)
self.flags.update_db = True
def reset_to_defaults(self):
if not self.doc_type:
return

View file

@ -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")

View file

@ -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;

View file

@ -92,7 +92,7 @@ class PostgresDatabase(Database):
# pylint: disable=W0221
def sql(self, *args, **kwargs):
if len(args):
if args:
# since tuple is immutable
args = list(args)
args[0] = modify_query(args[0])
@ -276,13 +276,13 @@ class PostgresDatabase(Database):
# pylint: disable=W1401
return self.sql('''
SELECT a.column_name AS name,
CASE a.data_type
CASE LOWER(a.data_type)
WHEN 'character varying' THEN CONCAT('varchar(', a.character_maximum_length ,')')
WHEN 'timestamp without TIME zone' THEN 'timestamp'
WHEN 'timestamp without time zone' THEN 'timestamp'
ELSE a.data_type
END AS type,
COUNT(b.indexdef) AS Index,
COALESCE(a.column_default, NULL) AS default,
SPLIT_PART(COALESCE(a.column_default, NULL), '::', 1) AS default,
BOOL_OR(b.unique) AS unique
FROM information_schema.columns a
LEFT JOIN

View file

@ -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")
) ;

View file

@ -13,7 +13,7 @@ class DBTable:
def __init__(self, doctype, meta=None):
self.doctype = doctype
self.table_name = 'tab{}'.format(doctype)
self.meta = meta or frappe.get_meta(doctype)
self.meta = meta or frappe.get_meta(doctype, False)
self.columns = {}
self.current_columns = {}
@ -65,64 +65,35 @@ class DBTable:
"""
get columns from docfields and custom fields
"""
fl = frappe.db.sql("SELECT * FROM `tabDocField` WHERE parent = %s", self.doctype, as_dict = 1)
lengths = {}
precisions = {}
uniques = {}
fields = self.meta.get_fieldnames_with_value(True)
# optional fields like _comments
if not self.meta.istable:
if not self.meta.get('istable'):
for fieldname in frappe.db.OPTIONAL_COLUMNS:
fl.append({
fields.append({
"fieldname": fieldname,
"fieldtype": "Text"
})
# add _seen column if track_seen
if getattr(self.meta, 'track_seen', False):
fl.append({
if self.meta.get('track_seen'):
fields.append({
'fieldname': '_seen',
'fieldtype': 'Text'
})
if (not frappe.flags.in_install_db
and (frappe.flags.in_install != "frappe"
or frappe.flags.ignore_in_install)):
custom_fl = frappe.db.sql("""
SELECT * FROM `tabCustom Field`
WHERE dt = %s AND docstatus < 2
""", (self.doctype,), as_dict=1)
if custom_fl: fl += custom_fl
# apply length, precision and unique from property setters
for ps in frappe.get_all("Property Setter",
fields=["field_name", "property", "value"],
filters={
"doc_type": self.doctype,
"doctype_or_field": "DocField",
"property": ["in", ["precision", "length", "unique"]]
}):
if ps.property=="length":
lengths[ps.field_name] = cint(ps.value)
elif ps.property=="precision":
precisions[ps.field_name] = cint(ps.value)
elif ps.property=="unique":
uniques[ps.field_name] = cint(ps.value)
for f in fl:
self.columns[f['fieldname']] = DbColumn(self,
f['fieldname'],
f['fieldtype'],
lengths.get(f["fieldname"]) or f.get('length'),
f.get('default'),
f.get('search_index'),
f.get('options'),
uniques.get(f["fieldname"],
f.get('unique')),
precisions.get(f['fieldname']) or f.get('precision'))
for field in fields:
self.columns[field.get('fieldname')] = DbColumn(
self,
field.get('fieldname'),
field.get('fieldtype'),
field.get('length'),
field.get('default'),
field.get('search_index'),
field.get('options'),
field.get('unique'),
field.get('precision')
)
def validate(self):
"""Check if change in varchar length isn't truncating the columns"""

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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",

View file

@ -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,

View file

@ -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,

View file

@ -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
}
}

View file

@ -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):

View file

@ -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()

View file

@ -77,9 +77,9 @@ def generate_report_result(report, filters=None, user=None):
if len(res) > 5:
skip_total_row = cint(res[5])
if report.custom_columns:
columns = json.loads(report.custom_columns)
result = add_data_to_custom_columns(columns, result)
if report.custom_columns:
columns = json.loads(report.custom_columns)
result = add_data_to_custom_columns(columns, result)
if result:
result = get_filtered_data(report.ref_doctype, columns, result, user)

View file

@ -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

View file

@ -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

View file

@ -87,6 +87,7 @@ permission_query_conditions = {
"ToDo": "frappe.desk.doctype.todo.todo.get_permission_query_conditions",
"User": "frappe.core.doctype.user.user.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 +102,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",

View file

@ -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:

View file

@ -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

View file

@ -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()
@ -978,7 +980,7 @@ class Document(BaseDocument):
def reset_seen(self):
"""Clear _seen property and set current user as seen"""
if getattr(self.meta, 'track_seen', False):
self.db_set('_seen', json.dumps([frappe.session.user]), update_modified=False)
frappe.db.set_value(self.doctype, self.name, "_seen", json.dumps([frappe.session.user]), update_modified=False)
def notify_update(self):
"""Publish realtime that the current document is modified"""

View file

@ -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', "Property Setter"):
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 + \
@ -290,17 +294,20 @@ class Meta(Document):
return get_workflow_name(self.name)
def add_custom_fields(self):
try:
self.extend("fields", frappe.db.sql("""SELECT * FROM `tabCustom Field`
WHERE dt = %s AND docstatus < 2""", (self.name,), as_dict=1,
update={"is_custom_field": 1}))
except Exception as e:
if frappe.db.is_table_missing(e):
return
else:
raise
if not frappe.db.table_exists('Custom Field'):
return
custom_fields = frappe.db.sql("""
SELECT * FROM `tabCustom Field`
WHERE dt = %s AND docstatus < 2
""", (self.name,), as_dict=1, update={"is_custom_field": 1})
self.extend("fields", custom_fields)
def apply_property_setters(self):
if not frappe.db.table_exists('Property Setter'):
return
property_setters = frappe.db.sql("""select * from `tabProperty Setter` where
doc_type=%s""", (self.name,), as_dict=1)
@ -378,8 +385,9 @@ class Meta(Document):
if custom_perms:
self.permissions = [Document(d) for d in custom_perms]
def get_fieldnames_with_value(self):
return [df.fieldname for df in self.fields if df.fieldtype not in no_value_fields]
def get_fieldnames_with_value(self, with_field_meta=False):
return [df if with_field_meta else df.fieldname \
for df in self.fields if df.fieldtype not in no_value_fields]
def get_fields_to_check_permissions(self, user_permission_doctypes):

View file

@ -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'''

View file

@ -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

View file

@ -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,

View file

@ -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 != ''
""")

View file

@ -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'''

View file

@ -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

View file

@ -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));

View file

@ -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;
}

View file

@ -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 => {

View file

@ -703,7 +703,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) {

View file

@ -10,6 +10,7 @@
</ul>
<ul class="list-unstyled sidebar-menu standard-actions">
{% if frappe.model.can_get_report(doctype) %}
<li class="list-sidebar-label">Views</li>
<li class="divider visible-sm visible-xs"></li>
<li class="list-link">
<div class="btn-group">
@ -64,7 +65,7 @@
<li class="list-stats list-link">
<div class="btn-group">
<a class = "dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" href="#" onclick="return false;">
{{ __("Tags") }}<span class="caret"></span>
{{ __("Tags") }} <span class="caret"></span>
</a>
<ul class="dropdown-menu list-stats-dropdown" role="menu">
<div class="dropdown-search">

View file

@ -54,19 +54,22 @@ frappe.views.ListGroupBy = class ListGroupBy {
render_group_by_items() {
let get_item_html = (fieldname) => {
let label;
let fieldtype;
if (fieldname === 'assigned_to') {
label = __('Assigned To');
} else if (fieldname === 'owner') {
label = __('Created By');
} else {
label = frappe.meta.get_label(this.doctype, fieldname);
fieldtype = frappe.meta.get_docfield(this.doctype, fieldname).fieldtype;
}
return `<li class="group-by-field list-link">
<div class="btn-group">
<a class = "dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
data-label="${label}" data-fieldname="${fieldname}" href="#" onclick="return false;">
${__(label)}<span class="caret"></span>
data-label="${label}" data-fieldname="${fieldname}" data-fieldtype="${fieldtype}"
href="#" onclick="return false;">
${__(label)} <span class="caret"></span>
</a>
<ul class="dropdown-menu group-by-dropdown" role="menu">
<li><div class="list-loading text-center group-by-loading text-muted">
@ -85,9 +88,10 @@ frappe.views.ListGroupBy = class ListGroupBy {
this.$wrapper.on('click', '.group-by-field', (e)=> {
let dropdown = $(e.currentTarget).find('.group-by-dropdown');
let fieldname = $(e.currentTarget).find('a').attr('data-fieldname');
let fieldtype = $(e.currentTarget).find('a').attr('data-fieldtype');
this.get_group_by_count(fieldname).then(field_count_list => {
if (field_count_list.length) {
this.render_dropdown_items(field_count_list, dropdown);
this.render_dropdown_items(field_count_list, fieldtype, dropdown);
this.sidebar.setup_dropdown_search(dropdown, '.group-by-value');
} else {
dropdown.find('.group-by-loading').html(`${__("No filters found")}`);
@ -98,7 +102,7 @@ frappe.views.ListGroupBy = class ListGroupBy {
get_group_by_dropdown_fields() {
let group_by_fields = [];
let fields = this.list_view.meta.fields.filter((f)=> ["Select", "Link"].includes(f.fieldtype));
let fields = this.list_view.meta.fields.filter((f)=> ["Select", "Link", "Data", "Int", "Check"].includes(f.fieldtype));
group_by_fields.push({
label: __(this.doctype),
fieldname: 'group_by_fields',
@ -118,7 +122,8 @@ frappe.views.ListGroupBy = class ListGroupBy {
let current_filters = this.list_view.get_filters_for_args();
// remove filter of the current field
current_filters = current_filters.filter((f_arr) => !f_arr.includes(field === 'assigned_to' ? '_assign': field));
current_filters =
current_filters.filter((f_arr) => !f_arr.includes(field === 'assigned_to' ? '_assign': field));
let args = {
doctype: this.doctype,
@ -138,11 +143,13 @@ frappe.views.ListGroupBy = class ListGroupBy {
});
}
render_dropdown_items(fields, dropdown) {
render_dropdown_items(fields, fieldtype, dropdown) {
let get_dropdown_html = (field) => {
let label = field.name == null ? __('Not Specified') : field.name;
if (label === frappe.session.user) {
label = __('Me');
} else if (fieldtype && fieldtype == 'Check') {
label = label == '0'? __('No'): __('Yes');
}
let value = field.name == null ? '' : encodeURIComponent(field.name);
@ -167,7 +174,9 @@ frappe.views.ListGroupBy = class ListGroupBy {
this.$wrapper.on('click', '.group-by-item', (e) => {
let $target = $(e.currentTarget);
let fieldname = $target.parents('.group-by-field').find('a').data('fieldname');
let value = decodeURIComponent($target.data('value').trim());
let value = typeof $target.data('value') === 'string'
? decodeURIComponent($target.data('value').trim())
: $target.data('value');
fieldname = fieldname === 'assigned_to' ? '_assign': fieldname;
return this.list_view.filter_area.remove(fieldname)

View file

@ -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));

View file

@ -252,7 +252,7 @@ frappe.ui.GroupBy = class {
this.group_by_fields = {};
this.all_fields = {};
let fields = this.report_view.meta.fields.filter(f => ["Select", "Link", "Data", "Int"].includes(f.fieldtype));
let fields = this.report_view.meta.fields.filter(f => ["Select", "Link", "Data", "Int", "Check"].includes(f.fieldtype));
this.group_by_fields[this.doctype] = fields;
this.all_fields[this.doctype] = this.report_view.meta.fields;

View file

@ -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';

View file

@ -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;

View file

@ -298,6 +298,55 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
}, 1000);
}
refresh_filters_dependency() {
this.filters.forEach(filter => {
filter.guardian_has_value = true;
if (filter.df.depends_on) {
filter.guardian_has_value =
this.evaluate_depends_on_value(filter.df.depends_on, filter.df.label);
if (filter.guardian_has_value) {
if (filter.df.hidden_due_to_dependency) {
filter.df.hidden_due_to_dependency = false;
this.toggle_filter_display(filter.df.fieldname, false);
}
} else {
if (!filter.df.hidden_due_to_dependency) {
filter.df.hidden_due_to_dependency = true;
this.toggle_filter_display(filter.df.fieldname, true);
filter.set_value(filter.df.default || null);
}
}
}
});
}
evaluate_depends_on_value(expression, filter_label) {
let out = null;
let filters = this.get_filter_values();
if (filters) {
if (typeof expression === 'boolean') {
out = expression;
} else if (expression.substr(0, 5) == 'eval:') {
try {
out = eval(expression.substr(5));
} catch (e) {
frappe.throw(__(`Invalid "depends_on" expression set in filter ${filter_label}`));
}
} else {
var value = filters[expression];
if ($.isArray(value)) {
out = !!value.length;
} else {
out = !!value;
}
}
}
return out;
}
setup_filters() {
this.clear_filters();
const { filters = [] } = this.report_settings;
@ -315,6 +364,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
if (df.on_change) f.on_change = df.on_change;
df.onchange = () => {
this.refresh_filters_dependency();
let current_filters = this.get_filter_value();
if (this.previous_filters
&& (JSON.stringify(this.previous_filters) === JSON.stringify(current_filters))) {
@ -344,6 +395,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
}).filter(Boolean);
this.refresh_filters_dependency();
if (this.filters.length === 0) {
// hide page form if no filters
this.page.hide_form();
@ -1472,8 +1524,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
}
}
toggle_filter(fieldname, flag) {
$(`div[data-fieldname=${fieldname}]`).toggleClass('hide-control', flag);
toggle_filter_display(fieldname, flag) {
this.$page.find(`div[data-fieldname=${fieldname}]`).toggleClass('hide-control', flag);
}
toggle_report(flag) {

View file

@ -17,3 +17,9 @@
<div class="no-image bg-light {{ class }} " {% if size %}style="width: {{size}}; height: {{size}};"{% endif %}></div>
{% endif %}
{% endmacro %}
{%- macro inspect(var, render=True) -%}
{%- if render -%}
<pre>{{ var | pprint | e }}</pre>
{%- endif -%}
{%- endmacro %}

View file

@ -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;
}

View file

@ -0,0 +1,70 @@
import unittest
import frappe
from frappe.core.utils import find
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
class TestDBUpdate(unittest.TestCase):
def test_db_update(self):
doctype = 'User'
frappe.reload_doctype('User', force=True)
frappe.model.meta.trim_tables('User')
make_property_setter(doctype, 'bio', 'fieldtype', 'Text', 'Data')
make_property_setter(doctype, 'enabled', 'default', '1', 'Int')
frappe.db.updatedb(doctype)
field_defs = get_field_defs(doctype)
table_columns = frappe.db.get_table_columns_description('tab{}'.format(doctype))
self.assertEqual(len(field_defs), len(table_columns))
for field_def in field_defs:
fieldname = field_def.get('fieldname')
table_column = find(table_columns, lambda d: d.get('name') == fieldname)
fieldtype = get_fieldtype_from_def(field_def)
fallback_default = '0' if field_def.get('fieldtype') in frappe.model.numeric_fieldtypes else 'NULL'
default = field_def.default if field_def.default is not None else fallback_default
self.assertEqual(fieldtype, table_column.type)
self.assertIn(table_column.default or 'NULL', [default, "'{}'".format(default)])
def get_fieldtype_from_def(field_def):
fieldtuple = frappe.db.type_map.get(field_def.fieldtype, ('', 0))
fieldtype = fieldtuple[0]
if fieldtype in ('varchar', 'datetime', 'int'):
fieldtype += '({})'.format(field_def.length or fieldtuple[1])
return fieldtype
def get_field_defs(doctype):
meta = frappe.get_meta(doctype, cached=False)
field_defs = meta.get_fieldnames_with_value(True)
field_defs += get_other_fields_meta(meta)
return field_defs
def get_other_fields_meta(meta):
default_fields_map = {
'name': ('Data', 0),
'owner': ('Data', 0),
'parent': ('Data', 0),
'parentfield': ('Data', 0),
'modified_by': ('Data', 0),
'parenttype': ('Data', 0),
'creation': ('Datetime', 0),
'modified': ('Datetime', 0),
'idx': ('Int', 8),
'docstatus': ('Check', 0)
}
optional_fields = frappe.db.OPTIONAL_COLUMNS
if meta.track_seen:
optional_fields.append('_seen')
optional_fields_map = {field: ('Text', 0) for field in optional_fields}
fields = dict(default_fields_map, **optional_fields_map)
field_map = [frappe._dict({'fieldname': field, 'fieldtype': _type, 'length': _length}) for field, (_type, _length) in fields.items()]
return field_map

View file

@ -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

View file

@ -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"}}

View file

@ -1,7 +1,9 @@
from __future__ import unicode_literals
import frappe
import json, re
import bleach, bleach_whitelist.bleach_whitelist as bleach_whitelist
import json
import re
import bleach
import bleach_whitelist.bleach_whitelist as bleach_whitelist
from six import string_types
from bs4 import BeautifulSoup
@ -47,7 +49,7 @@ def clean_script_and_style(html):
def sanitize_html(html, linkify=False):
"""
Sanitize HTML tags, attributes and style to prevent XSS attacks
Based on bleach clean, bleach whitelist and HTML5lib's Sanitizer defaults
Based on bleach clean, bleach whitelist and html5lib's Sanitizer defaults
Does not sanitize JSON, as it could lead to future problems
"""

View file

@ -16,6 +16,7 @@ import frappe
from frappe import _
from frappe.utils import get_wkhtmltopdf_version, scrub_urls
PDF_CONTENT_ERRORS = ["ContentNotFoundError", "ContentOperationNotPermittedError",
"UnknownContentError", "RemoteHostClosedError"]

View file

@ -31,6 +31,11 @@ def get_context(path, args=None):
if hasattr(frappe.local, 'response') and frappe.local.response.get('context'):
context.update(frappe.local.response.context)
# to be able to inspect the context in development
# Use the macro "inspect" from macros.html
if frappe.conf.developer_mode:
context._context_dict = context
return context
def update_controller_context(context, controller):

View file

@ -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))

View file

@ -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)
@ -287,7 +308,9 @@ def extract_title(source, path):
if not title and "<h1>" in source:
# extract title from h1
match = re.findall('<h1>([^<]*)', source)
title = match[0].strip()[:300]
title_content = match[0].strip()[:300]
if '{{' not in title_content:
title = title_content
if not title:
# make title from name

View file

@ -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

View file

@ -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 %}

View file

@ -1,7 +1,7 @@
Babel==2.6.0
beautifulsoup4==4.8.2
bleach-whitelist==0.0.10
bleach==2.1.4
bleach==3.1.2
boto3==1.10.18
braintree==3.57.1
chardet==3.0.4
@ -23,8 +23,10 @@ google-auth==1.7.1
googlemaps==3.1.1
gunicorn==19.10.0
html2text==2016.9.19
html5lib==1.0.1
ipython==5.9.0
Jinja2==2.10.3
ldap3==2.7
markdown2==2.3.8
maxminddb-geolite2==2018.703
ndg-httpsclient==0.5.1