Merge branch 'develop' of github.com:frappe/frappe into desk-user-custom

This commit is contained in:
Shivam Mishra 2020-04-08 18:12:43 +05:30
commit 4764f65c13
120 changed files with 2123 additions and 1552 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,

17
CODEOWNERS Normal file
View 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

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

@ -1,4 +1,5 @@
{
"actions": [],
"autoname": "REP.#####",
"creation": "2018-06-25 18:39:11.152960",
"doctype": "DocType",
@ -101,7 +102,8 @@
}
],
"in_create": 1,
"modified": "2019-09-18 04:00:55.644257",
"links": [],
"modified": "2020-03-05 10:52:56.598365",
"modified_by": "Administrator",
"module": "Core",
"name": "Prepared Report",
@ -118,6 +120,15 @@
"role": "System Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Prepared Report User",
"share": 1
}
],
"quick_entry": 1,

View file

@ -98,3 +98,34 @@ def download_attachment(dn):
attached_file = frappe.get_doc("File", attachment.name)
frappe.local.response.filecontent = gzip_decompress(attached_file.get_content())
frappe.local.response.type = "binary"
def get_permission_query_condition(user):
if not user: user = frappe.session.user
if user == "Administrator":
return None
from frappe.utils.user import UserPermissions
user = UserPermissions(user)
if "System Manager" in user.roles:
return None
reports = [frappe.db.escape(report) for report in user.get_all_reports().keys()]
return """`tabPrepared Report`.ref_report_doctype in ({reports})"""\
.format(reports=','.join(reports))
def has_permission(doc, user):
if not user: user = frappe.session.user
if user == "Administrator":
return True
from frappe.utils.user import UserPermissions
user = UserPermissions(user)
if "System Manager" in user.roles:
return True
return doc.ref_report_doctype in user.get_all_reports().keys()

View file

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

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

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

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

@ -101,7 +101,8 @@ class User(Document):
frappe.enqueue(
'frappe.core.doctype.user.user.create_contact',
user=self,
ignore_mandatory=True
ignore_mandatory=True,
now=frappe.flags.in_test
)
if self.name not in ('Administrator', 'Guest') and not self.user_image:
frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name)
@ -554,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)
@ -1038,8 +1040,8 @@ def create_contact(user, ignore_links=False, ignore_mandatory=False):
from frappe.contacts.doctype.contact.contact import get_contact_name
if user.name in ["Administrator", "Guest"]: return
contact_exists = get_contact_name(user.email)
if not contact_exists:
contact_name = get_contact_name(user.email)
if not contact_name:
contact = frappe.get_doc({
"doctype": "Contact",
"first_name": user.first_name,
@ -1058,7 +1060,7 @@ def create_contact(user, ignore_links=False, ignore_mandatory=False):
contact.add_phone(user.mobile_no, is_primary_mobile_no=True)
contact.insert(ignore_permissions=True, ignore_links=ignore_links, ignore_mandatory=ignore_mandatory)
else:
contact = frappe.get_doc("Contact", contact_exists)
contact = frappe.get_doc("Contact", contact_name)
contact.first_name = user.first_name
contact.last_name = user.last_name
contact.gender = user.gender

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,36 +76,48 @@ 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,
options: {
allow_sorting: false,
allow_create: false,
allow_delete: false,
allow_hiding: false,
allow_edit: 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,
options: {
allow_sorting: false,
allow_create: false,
allow_delete: false,
allow_hiding: false,
allow_edit: 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() {

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

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

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

@ -163,8 +163,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

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

View file

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

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

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

@ -6,14 +6,13 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.desk.doctype.notification_settings.notification_settings import (is_notifications_enabled,
is_email_notifications_enabled, is_email_notifications_enabled_for_type, set_seen_value)
from frappe.desk.doctype.notification_settings.notification_settings import (is_notifications_enabled, is_email_notifications_enabled_for_type, set_seen_value)
class NotificationLog(Document):
def after_insert(self):
frappe.publish_realtime('notification', after_commit=True, user=self.for_user)
set_notifications_as_unseen(self.for_user)
if is_email_notifications_enabled(self.for_user):
if is_email_notifications_enabled_for_type(self.for_user, self.type):
send_notification_email(self)
@ -73,9 +72,6 @@ def make_notification_logs(doc, users):
_doc.insert(ignore_permissions=True)
def send_notification_email(doc):
is_type_enabled = is_email_notifications_enabled_for_type(doc.for_user, doc.type)
if not is_type_enabled:
return
if doc.type == 'Energy Point' and doc.email_content is None:
return

View file

@ -25,6 +25,9 @@ def is_email_notifications_enabled(user):
return enabled
def is_email_notifications_enabled_for_type(user, notification_type):
if not is_email_notifications_enabled(user):
return False
fieldname = 'enable_email_' + frappe.scrub(notification_type)
enabled = frappe.db.get_value('Notification Settings', user, fieldname)
if enabled is None:

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

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

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

@ -1,120 +1,70 @@
{
"allow_copy": 0,
"allow_import": 1,
"allow_rename": 0,
"autoname": "field:title",
"beta": 0,
"creation": "2015-03-18 06:08:32.729800",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 0,
"actions": [],
"allow_import": 1,
"autoname": "field:title",
"creation": "2015-03-18 06:08:32.729800",
"doctype": "DocType",
"document_type": "Setup",
"field_order": [
"title",
"total_subscribers",
"confirmation_email_template",
"welcome_email_template"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "title",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Title",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"no_copy": 1,
"reqd": 1,
"unique": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "total_subscribers",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Total Subscribers",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"default": "0",
"fieldname": "total_subscribers",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Total Subscribers",
"read_only": 1
},
{
"fieldname": "confirmation_email_template",
"fieldtype": "Link",
"label": "Confirmation Email Template",
"options": "Email Template"
},
{
"fieldname": "welcome_email_template",
"fieldtype": "Link",
"label": "Welcome Email Template",
"options": "Email Template"
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-02-27 19:01:17.203845",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Group",
"name_case": "",
"owner": "Administrator",
],
"links": [],
"modified": "2020-02-21 14:12:48.884738",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Group",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 1,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Newsletter Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"import": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Newsletter Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
],
"quick_entry": 1,
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -66,6 +66,11 @@ def import_from(name, doctype):
def add_subscribers(name, email_list):
if not isinstance(email_list, (list, tuple)):
email_list = email_list.replace(",", "\n").split("\n")
template = frappe.db.get_value('Email Group', name, 'welcome_email_template')
if template:
welcome_email = frappe.get_doc("Email Template", template)
count = 0
for email in email_list:
email = email.strip()
@ -78,7 +83,9 @@ def add_subscribers(name, email_list):
"doctype": "Email Group Member",
"email_group": name,
"email": parsed_email
}).insert(ignore_permissions = frappe.flags.ignore_permissions)
}).insert(ignore_permissions=frappe.flags.ignore_permissions)
send_welcome_email(welcome_email, parsed_email, name)
count += 1
else:
@ -90,3 +97,15 @@ def add_subscribers(name, email_list):
return frappe.get_doc("Email Group", name).update_total_subscribers()
def send_welcome_email(welcome_email, email, email_group):
"""Send welcome email for the subscribers of a given email group."""
if not welcome_email:
return
args = dict(
email=email,
email_group=email_group
)
message = frappe.render_template(welcome_email.response, args)
frappe.sendmail(email, subject=welcome_email.subject, message=message)

View file

@ -4,23 +4,65 @@
frappe.ui.form.on('Newsletter', {
refresh(frm) {
let doc = frm.doc;
if(!doc.__islocal && !cint(doc.email_sent) && !doc.__unsaved
if (!doc.__islocal && !cint(doc.email_sent) && !doc.__unsaved
&& in_list(frappe.boot.user.can_write, doc.doctype)) {
frm.add_custom_button(__('Send'), function() {
frm.call('send_emails').then(() => {
frm.refresh();
frm.add_custom_button(__('Send Now'), function() {
frappe.confirm(__("Do you really want to send this email newsletter?"), function() {
frm.call('send_emails').then(() => {
frm.refresh();
});
});
}, "fa fa-play", "btn-success");
}
if (!doc.__islocal && cint(doc.email_sent)) {
frm.set_df_property('schedule_send', "read_only", 1);
}
frm.events.setup_dashboard(frm);
if(doc.__islocal && !doc.send_from) {
if (doc.__islocal && !doc.send_from) {
let { fullname, email } = frappe.user_info(doc.owner);
frm.set_value('send_from', `${fullname} <${email}>`);
}
},
onload_post_render(frm) {
frm.trigger('setup_schedule_send');
},
setup_schedule_send(frm) {
let today = new Date();
// setting datepicker options to set min date & min time
today.setHours(today.getHours() + 1 );
frm.get_field('schedule_send').$input.datepicker({
maxMinutes: 0,
minDate: today,
timeFormat: 'hh:00:00',
onSelect: function (fd, d, picker) {
if (!d) return;
var date = d.toDateString();
if (date === today.toDateString()) {
picker.update({
minHours: (today.getHours() + 1)
});
} else {
picker.update({
minHours: 0
});
}
frm.get_field('schedule_send').$input.trigger('change');
}
});
const $tp = frm.get_field('schedule_send').datepicker.timepicker;
$tp.$minutes.parent().css('display', 'none');
$tp.$minutesText.css('display', 'none');
$tp.$minutesText.prev().css('display', 'none');
$tp.$seconds.parent().css('display', 'none');
},
setup_dashboard(frm) {
if(!frm.doc.__islocal && cint(frm.doc.email_sent)
&& frm.doc.__onload && frm.doc.__onload.status_count) {

View file

@ -1,4 +1,5 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2013-01-10 16:34:31",
"description": "Create and Send Newsletters",
@ -6,9 +7,11 @@
"document_type": "Other",
"engine": "InnoDB",
"field_order": [
"send_from",
"column_break_2",
"schedule_send",
"recipients",
"email_group",
"send_from",
"email_sent",
"newsletter_content",
"subject",
@ -41,7 +44,7 @@
"default": "0",
"fieldname": "email_sent",
"fieldtype": "Check",
"label": "Email Sent?",
"label": "Email Sent",
"no_copy": 1,
"read_only": 1
},
@ -115,14 +118,24 @@
"fieldname": "recipients",
"fieldtype": "Section Break",
"label": "Recipients"
},
{
"fieldname": "schedule_send",
"fieldtype": "Datetime",
"label": "Schedule Send"
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
}
],
"has_web_view": 1,
"icon": "fa fa-envelope",
"idx": 1,
"is_published_field": "published",
"links": [],
"max_attachments": 3,
"modified": "2019-09-06 22:15:55.471254",
"modified": "2020-03-02 06:26:51.622521",
"modified_by": "Administrator",
"module": "Email",
"name": "Newsletter",

View file

@ -11,7 +11,7 @@ from frappe.utils.verified_command import get_signed_params, verify_request
from frappe.utils.background_jobs import enqueue
from frappe.email.queue import send
from frappe.email.doctype.email_group.email_group import add_subscribers
from frappe.utils import parse_addr
from frappe.utils import parse_addr, now_datetime
from frappe.utils import validate_email_address
@ -42,7 +42,6 @@ class Newsletter(WebsiteGenerator):
if self.recipients:
if getattr(frappe.local, "is_ajax", False):
self.validate_send()
# using default queue with a longer timeout as this isn't a scheduled task
enqueue(send_newsletter, queue='default', timeout=6000, event='send_newsletter',
newsletter=self.name)
@ -53,6 +52,7 @@ class Newsletter(WebsiteGenerator):
frappe.msgprint(_("Scheduled to send to {0} recipients").format(len(self.recipients)))
frappe.db.set(self, "email_sent", 1)
frappe.db.set(self, "schedule_send", now_datetime())
frappe.db.set(self, 'scheduled_to_send', len(self.recipients))
else:
frappe.msgprint(_("Newsletter should have atleast one recipient"))
@ -160,39 +160,52 @@ def create_lead(email_id):
@frappe.whitelist(allow_guest=True)
def subscribe(email):
def subscribe(email, email_group=_('Website')):
url = frappe.utils.get_url("/api/method/frappe.email.doctype.newsletter.newsletter.confirm_subscription") +\
"?" + get_signed_params({"email": email})
"?" + get_signed_params({"email": email, "email_group": email_group})
messages = (
_("Thank you for your interest in subscribing to our updates"),
_("Please verify your Email Address"),
url,
_("Click here to verify")
)
email_template = frappe.db.get_value('Email Group', email_group, ['confirmation_email_template'])
content = """
<p>{0}. {1}.</p>
<p><a href="{2}">{3}</a></p>
"""
content=''
if email_template:
args = dict(
email=email,
confirmation_url=url,
email_group=email_group
)
frappe.sendmail(email, subject=_("Confirm Your Email"), content=content.format(*messages))
email_template = frappe.get_doc("Email Template", email_template)
content = frappe.render_template(email_template.response, args)
if not content:
messages = (
_("Thank you for your interest in subscribing to our updates"),
_("Please verify your Email Address"),
url,
_("Click here to verify")
)
content = """
<p>{0}. {1}.</p>
<p><a href="{2}">{3}</a></p>
""".format(*messages)
frappe.sendmail(email, subject=getattr('email_template', 'subject', '') or _("Confirm Your Email"), content=content)
@frappe.whitelist(allow_guest=True)
def confirm_subscription(email):
def confirm_subscription(email, email_group=_('Website')):
if not verify_request():
return
if not frappe.db.exists("Email Group", _("Website")):
if not frappe.db.exists("Email Group", email_group):
frappe.get_doc({
"doctype": "Email Group",
"title": _("Website")
"title": email_group
}).insert(ignore_permissions=True)
frappe.flags.ignore_permissions = True
add_subscribers(_("Website"), email)
add_subscribers(email_group, email)
frappe.db.commit()
frappe.respond_as_web_page(_("Confirmed"),
@ -212,7 +225,7 @@ def send_newsletter(newsletter):
doc.db_set("email_sent", 0)
frappe.db.commit()
frappe.log_error("send_newsletter")
frappe.log_error(title='Send Newsletter')
raise
@ -250,3 +263,11 @@ def get_newsletter_list(doctype, txt, filters, limit_start, limit_page_length=20
'''.format(','.join(['%s'] * len(email_group_list)),
limit_page_length, limit_start), email_group_list, as_dict=1)
def send_scheduled_email():
"""Send scheduled newsletter to the recipients."""
scheduled_newsletter = frappe.get_all('Newsletter', filters = {
'schedule_send': ('<=', now_datetime()),
'email_sent': 0
}, fields = ['name'])
for newsletter in scheduled_newsletter:
send_newsletter(newsletter.name)

View file

@ -3,8 +3,9 @@
from __future__ import unicode_literals
import frappe, unittest
from frappe.utils import getdate, add_days
from frappe.email.doctype.newsletter.newsletter import confirmed_unsubscribe
from frappe.email.doctype.newsletter.newsletter import confirmed_unsubscribe, send_scheduled_email
from six.moves.urllib.parse import unquote
test_dependencies = ["Email Group"]
@ -58,7 +59,7 @@ class TestNewsletter(unittest.TestCase):
self.assertTrue(email in recipients)
@staticmethod
def send_newsletter(published=0):
def send_newsletter(published=0, schedule_send=None):
frappe.db.sql("delete from `tabEmail Queue`")
frappe.db.sql("delete from `tabEmail Queue Recipient`")
frappe.db.sql("delete from `tabNewsletter`")
@ -67,11 +68,16 @@ class TestNewsletter(unittest.TestCase):
"subject": "_Test Newsletter",
"send_from": "Test Sender <test_sender@example.com>",
"message": "Testing my news.",
"published": published
"published": published,
"schedule_send": schedule_send
}).insert(ignore_permissions=True)
newsletter.append("email_group", {"email_group": "_Test Email Group"})
newsletter.save()
if schedule_send:
send_scheduled_email()
return
newsletter.send_emails()
return newsletter.name
@ -89,4 +95,13 @@ class TestNewsletter(unittest.TestCase):
doc = frappe.get_doc("Newsletter", newsletter_name)
doc.get_context(context)
self.assertEqual(context.no_cache, 1)
self.assertTrue("attachments" not in list(context))
self.assertTrue("attachments" not in list(context))
def test_schedule_send(self):
self.send_newsletter(schedule_send=add_days(getdate(), -1))
email_queue_list = [frappe.get_doc('Email Queue', e.name) for e in frappe.get_all("Email Queue")]
self.assertEqual(len(email_queue_list), 4)
recipients = [e.recipients[0].recipient for e in email_queue_list]
for email in emails:
self.assertTrue(email in recipients)

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

@ -86,14 +86,17 @@ 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",
"Contact": "frappe.contacts.address_and_contact.get_permission_query_conditions_for_contact",
"Address": "frappe.contacts.address_and_contact.get_permission_query_conditions_for_address",
"Communication": "frappe.core.doctype.communication.communication.get_permission_query_conditions_for_communication",
"Workflow Action": "frappe.workflow.doctype.workflow_action.workflow_action.get_permission_query_conditions"
"Workflow Action": "frappe.workflow.doctype.workflow_action.workflow_action.get_permission_query_conditions",
"Prepared Report": "frappe.core.doctype.prepared_report.prepared_report.get_permission_query_condition"
}
has_permission = {
@ -101,12 +104,14 @@ 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",
"Communication": "frappe.core.doctype.communication.communication.has_permission",
"Workflow Action": "frappe.workflow.doctype.workflow_action.workflow_action.has_permission",
"File": "frappe.core.doctype.file.file.has_permission"
"File": "frappe.core.doctype.file.file.has_permission",
"Prepared Report": "frappe.core.doctype.prepared_report.prepared_report.has_permission"
}
has_website_permission = {
@ -184,7 +189,8 @@ scheduler_events = {
"frappe.desk.page.backups.backups.delete_downloadable_backups",
"frappe.deferred_insert.save_to_db",
"frappe.desk.form.document_follow.send_hourly_updates",
"frappe.integrations.doctype.google_calendar.google_calendar.sync"
"frappe.integrations.doctype.google_calendar.google_calendar.sync",
"frappe.email.doctype.newsletter.newsletter.send_scheduled_email"
],
"daily": [
"frappe.email.queue.clear_outbox",

View file

@ -1,487 +1,129 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2016-09-21 10:12:57.399174",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "System",
"editable_grid": 1,
"creation": "2016-09-21 10:12:57.399174",
"doctype": "DocType",
"document_type": "System",
"editable_grid": 1,
"field_order": [
"enabled",
"send_notifications_to",
"send_email_for_successful_backup",
"backup_frequency",
"limit_no_of_backups",
"no_of_backups",
"file_backup",
"app_access_key",
"app_secret_key",
"allow_dropbox_access",
"dropbox_access_key",
"dropbox_access_secret",
"dropbox_access_token"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "enabled",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Enabled",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"default": "0",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "send_notifications_to",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Send Notifications To",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "send_notifications_to",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Send Notifications To",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"description": "Note: By default emails for failed backups are sent.",
"fieldname": "send_email_for_successful_backup",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Send Email for Successful Backup",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"default": "1",
"description": "Note: By default emails for failed backups are sent.",
"fieldname": "send_email_for_successful_backup",
"fieldtype": "Check",
"label": "Send Email for Successful Backup"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "backup_frequency",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Backup Frequency",
"length": 0,
"no_copy": 0,
"options": "\nDaily\nWeekly",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "backup_frequency",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Backup Frequency",
"options": "\nDaily\nWeekly",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "limit_no_of_backups",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Limit Number of DB Backups",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"default": "0",
"fieldname": "limit_no_of_backups",
"fieldtype": "Check",
"label": "Limit Number of DB Backups"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "5",
"depends_on": "eval:doc.limit_no_of_backups",
"fieldname": "no_of_backups",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Number of DB Backups",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"default": "5",
"depends_on": "eval:doc.limit_no_of_backups",
"fieldname": "no_of_backups",
"fieldtype": "Int",
"label": "Number of DB Backups"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "file_backup",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "File Backup",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"default": "1",
"fieldname": "file_backup",
"fieldtype": "Check",
"label": "File Backup"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "app_access_key",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "App Access Key",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "app_access_key",
"fieldtype": "Data",
"label": "App Access Key"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "app_secret_key",
"fieldtype": "Password",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "App Secret Key",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "app_secret_key",
"fieldtype": "Password",
"label": "App Secret Key"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "allow_dropbox_access",
"fieldtype": "Button",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Allow Dropbox Access",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "allow_dropbox_access",
"fieldtype": "Button",
"label": "Allow Dropbox Access"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "dropbox_access_key",
"fieldtype": "Password",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Dropbox Access Key",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "dropbox_access_key",
"fieldtype": "Password",
"hidden": 1,
"label": "Dropbox Access Key",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "dropbox_access_secret",
"fieldtype": "Password",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Dropbox Access Secret",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "dropbox_access_secret",
"fieldtype": "Password",
"hidden": 1,
"label": "Dropbox Access Secret",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "dropbox_access_token",
"fieldtype": "Password",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Dropbox Access Token",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldname": "dropbox_access_token",
"fieldtype": "Password",
"hidden": 1,
"label": "Dropbox Access Token"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 1,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2019-01-03 05:44:40.520943",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Dropbox Settings",
"name_case": "",
"owner": "Administrator",
],
"in_create": 1,
"issingle": 1,
"modified": "2019-08-22 16:26:44.468391",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Dropbox Settings",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 1,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}
],
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -3,22 +3,25 @@
# For license information, please see license.txt
from __future__ import unicode_literals
import dropbox
import json
import frappe
import os
from frappe import _
from frappe.model.document import Document
import dropbox, json
from frappe.integrations.offsite_backup_utils import get_latest_backup_file, send_email, validate_file_size
from frappe.integrations.utils import make_post_request
from frappe.utils import (cint, get_request_site_address,
get_files_path, get_backups_path, get_url, encode)
from frappe.utils.backups import new_backup
from frappe.utils.background_jobs import enqueue
from six.moves.urllib.parse import urlparse, parse_qs
from frappe.integrations.utils import make_post_request
from rq.timeouts import JobTimeoutException
from frappe.utils import (cint, split_emails, get_request_site_address,
get_files_path, get_backups_path, get_url, encode)
from six import text_type
ignore_list = [".DS_Store"]
class DropboxSettings(Document):
def onload(self):
if not self.app_access_key and frappe.conf.dropbox_access_key:
@ -48,10 +51,12 @@ def take_backup_to_dropbox(retry_count=0, upload_db_backup=True):
did_not_upload, error_log = [], []
try:
if cint(frappe.db.get_value("Dropbox Settings", None, "enabled")):
validate_file_size()
did_not_upload, error_log = backup_to_dropbox(upload_db_backup)
if did_not_upload: raise Exception
send_email(True, "Dropbox")
send_email(True, "Dropbox", "Dropbox Settings", "send_notifications_to")
except JobTimeoutException:
if retry_count < 2:
args = {
@ -66,34 +71,8 @@ def take_backup_to_dropbox(retry_count=0, upload_db_backup=True):
else:
file_and_error = [" - ".join(f) for f in zip(did_not_upload, error_log)]
error_message = ("\n".join(file_and_error) + "\n" + frappe.get_traceback())
frappe.errprint(error_message)
send_email(False, "Dropbox", error_message)
def send_email(success, service_name, error_status=None):
if success:
if frappe.db.get_value("Dropbox Settings", None, "send_email_for_successful_backup") == '0':
return
subject = "Backup Upload Successful"
message ="""<h3>Backup Uploaded Successfully</h3><p>Hi there, this is just to inform you
that your backup was successfully uploaded to your %s account. So relax!</p>
""" % service_name
else:
subject = "[Warning] Backup Upload Failed"
message ="""<h3>Backup Upload Failed</h3><p>Oops, your automated backup to %s
failed.</p>
<p>Error message: <br>
<pre><code>%s</code></pre>
</p>
<p>Please contact your system manager for more information.</p>
""" % (service_name, error_status)
if not frappe.db:
frappe.connect()
recipients = split_emails(frappe.db.get_value("Dropbox Settings", None, "send_notifications_to"))
frappe.sendmail(recipients=recipients, subject=subject, message=message)
send_email(False, "Dropbox", "Dropbox Settings", "send_notifications_to", error_message)
def backup_to_dropbox(upload_db_backup=True):
if not frappe.db:
@ -114,8 +93,12 @@ def backup_to_dropbox(upload_db_backup=True):
dropbox_client = dropbox.Dropbox(dropbox_settings['access_token'])
if upload_db_backup:
backup = new_backup(ignore_files=True)
filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db))
if frappe.flags.create_new_backup:
backup = new_backup(ignore_files=True)
filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db))
else:
filename = get_latest_backup_file()
upload_file_to_dropbox(filename, "/database", dropbox_client)
# delete older databases

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestDropboxSettings(unittest.TestCase):
pass

View file

@ -19,6 +19,7 @@ from apiclient.http import MediaFileUpload
from frappe.utils import get_backups_path, get_bench_path
from frappe.utils.backups import new_backup
from frappe.integrations.doctype.google_settings.google_settings import get_auth_url
from frappe.integrations.offsite_backup_utils import get_latest_backup_file, send_email, validate_file_size
SCOPES = "https://www.googleapis.com/auth/drive"
@ -183,13 +184,16 @@ def upload_system_backup_to_google_drive():
check_for_folder_in_google_drive()
account.load_from_db()
progress(1, "Backing up Data.")
backup = new_backup()
fileurl_backup = os.path.basename(backup.backup_path_db)
fileurl_public_files = os.path.basename(backup.backup_path_files)
fileurl_private_files = os.path.basename(backup.backup_path_private_files)
validate_file_size()
if frappe.flags.create_new_backup:
set_progress(1, "Backing up Data.")
backup = new_backup()
fileurl_backup = os.path.basename(backup.backup_path_db)
fileurl_public_files = os.path.basename(backup.backup_path_files)
fileurl_private_files = os.path.basename(backup.backup_path_private_files)
else:
fileurl_backup, fileurl_public_files, fileurl_private_files = get_latest_backup_file(with_files=True)
for fileurl in [fileurl_backup, fileurl_public_files, fileurl_private_files]:
file_metadata = {
@ -203,15 +207,14 @@ def upload_system_backup_to_google_drive():
frappe.throw(_("Google Drive - Could not locate locate - {0}").format(e))
try:
progress(2, "Uploading backup to Google Drive.")
set_progress(2, "Uploading backup to Google Drive.")
google_drive.files().create(body=file_metadata, media_body=media, fields="id").execute()
except HttpError as e:
send_email(success=False, error=e)
frappe.msgprint(_("Google Drive - Could not upload backup - Error {0}").format(e))
send_email(False, "Google Drive", "Google Drive", "email", error_status=e)
progress(3, "Uploading successful.")
set_progress(3, "Uploading successful.")
frappe.db.set_value("Google Drive", None, "last_backup_on", frappe.utils.now_datetime())
send_email(success=True)
send_email(True, "Google Drive", "Google Drive", "email")
return _("Google Drive Backup Successful.")
def daily_backup():
@ -226,30 +229,5 @@ def get_absolute_path(filename):
file_path = os.path.join(get_backups_path()[2:], filename)
return "{0}/sites/{1}".format(get_bench_path(), file_path)
def progress(progress, message):
def set_progress(progress, message):
frappe.publish_realtime("upload_to_google_drive", dict(progress=progress, total=3, message=message), user=frappe.session.user)
def send_email(success, error=None):
if success:
if not frappe.db.get_single_value("Google Drive", "send_email_for_successful_backup"):
return
subject = "Backup Upload Successful"
message = """<h3>Backup Uploaded Successfully</h3><p>Hi there, this is just to inform you
that your backup was successfully uploaded to Google Drive.</p>
"""
else:
subject = "[Warning] Backup Upload Failed"
message = """<h3>Backup Upload Failed</h3><p>Oops, your automated backup to Google Drive
failed.</p>
<p>Error message: <br>
<pre><code>{0}</code></pre>
</p>
<p>Please contact your system manager for more information.</p>
""".format(error)
frappe.sendmail(
recipients=frappe.db.get_single_value("Google Drive", "email"),
subject=subject,
message=message
)

View file

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

View file

@ -1,397 +1,110 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2017-09-04 20:57:20.129205",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"creation": "2017-09-04 20:57:20.129205",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enabled",
"notify_email",
"send_email_for_successful_backup",
"frequency",
"access_key_id",
"secret_access_key",
"region",
"endpoint_url",
"bucket",
"backup_limit"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "enabled",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Enable Automatic Backup",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"default": "0",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enable Automatic Backup"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "notify_email",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Send Notifications To",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "notify_email",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Send Notifications To",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"description": "Note: By default emails for failed backups are sent.",
"fetch_if_empty": 0,
"fieldname": "send_email_for_successful_backup",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Send Email for Successful Backup",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"default": "1",
"description": "Note: By default emails for failed backups are sent.",
"fieldname": "send_email_for_successful_backup",
"fieldtype": "Check",
"label": "Send Email for Successful Backup"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "frequency",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Backup Frequency",
"length": 0,
"no_copy": 0,
"options": "Daily\nWeekly\nMonthly\nNone",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "frequency",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Backup Frequency",
"options": "Daily\nWeekly\nMonthly\nNone",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "access_key_id",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Access Key ID",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "access_key_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Access Key ID",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "secret_access_key",
"fieldtype": "Password",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Secret Access Key",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "secret_access_key",
"fieldtype": "Password",
"in_list_view": 1,
"label": "Secret Access Key",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "us-east-1",
"description": "See https://docs.aws.amazon.com/de_de/general/latest/gr/rande.html#s3_region for details.",
"fetch_if_empty": 0,
"fieldname": "region",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Region",
"length": 0,
"no_copy": 0,
"options": "us-east-1\nus-east-2\nus-west-1\nus-west-2\nap-south-1\nap-southeast-1\nap-southeast-2\nap-northeast-1\nap-northeast-2\nap-northeast-3\nca-central-1\ncn-north-1\ncn-northwest-1\neu-central-1\neu-west-1\neu-west-2\neu-west-3\neu-north-1\nsa-east-1",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"default": "us-east-1",
"description": "See https://docs.aws.amazon.com/de_de/general/latest/gr/rande.html#s3_region for details.",
"fieldname": "region",
"fieldtype": "Select",
"label": "Region",
"options": "us-east-1\nus-east-2\nus-west-1\nus-west-2\nap-south-1\nap-southeast-1\nap-southeast-2\nap-northeast-1\nap-northeast-2\nap-northeast-3\nca-central-1\ncn-north-1\ncn-northwest-1\neu-central-1\neu-west-1\neu-west-2\neu-west-3\neu-north-1\nsa-east-1"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "endpoint_url",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Endpoint URL",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "endpoint_url",
"fieldtype": "Data",
"label": "Endpoint URL"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "bucket",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Bucket",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "bucket",
"fieldtype": "Data",
"label": "Bucket",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "backup_limit",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Backup Limit",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldname": "backup_limit",
"fieldtype": "Int",
"label": "Backup Limit",
"reqd": 1
}
],
"has_web_view": 0,
"hide_toolbar": 1,
"idx": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
"modified": "2019-04-10 03:56:55.632017",
"modified_by": "Administrator",
"module": "Integrations",
"name": "S3 Backup Settings",
"name_case": "",
"owner": "Administrator",
],
"hide_toolbar": 1,
"issingle": 1,
"modified": "2019-08-22 16:26:04.774571",
"modified_by": "Administrator",
"module": "Integrations",
"name": "S3 Backup Settings",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -8,12 +8,14 @@ import os.path
import frappe
import boto3
from frappe import _
from frappe.integrations.offsite_backup_utils import get_latest_backup_file, send_email, validate_file_size
from frappe.model.document import Document
from frappe.utils import cint, split_emails
from frappe.utils import cint
from frappe.utils.background_jobs import enqueue
from rq.timeouts import JobTimeoutException
from botocore.exceptions import ClientError
class S3BackupSettings(Document):
def validate(self):
@ -49,7 +51,7 @@ class S3BackupSettings(Document):
@frappe.whitelist()
def take_backup():
"Enqueue longjob for taking backup to s3"
"""Enqueue longjob for taking backup to s3"""
enqueue("frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_s3", queue='long', timeout=1500)
frappe.msgprint(_("Queued for backup. It may take a few minutes to an hour."))
@ -65,22 +67,21 @@ def take_backups_weekly():
def take_backups_monthly():
take_backups_if("Monthly")
def take_backups_if(freq):
if cint(frappe.db.get_value("S3 Backup Settings", None, "enabled")):
if frappe.db.get_value("S3 Backup Settings", None, "frequency") == freq:
take_backups_s3()
@frappe.whitelist()
def take_backups_s3(retry_count=0):
try:
validate_file_size()
backup_to_s3()
send_email(True, "S3 Backup Settings")
send_email(True, "Amazon S3", "S3 Backup Settings", "notify_email")
except JobTimeoutException:
if retry_count < 2:
args = {
"retry_count" :retry_count + 1
"retry_count": retry_count + 1
}
enqueue("frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_s3",
queue='long', timeout=1500, **args)
@ -89,31 +90,10 @@ def take_backups_s3(retry_count=0):
except Exception:
notify()
def notify():
error_message = frappe.get_traceback()
frappe.errprint(error_message)
send_email(False, "S3 Backup Settings", error_message)
def send_email(success, service_name, error_status=None):
if success:
if frappe.db.get_value("S3 Backup Settings", None, "send_email_for_successful_backup") == '0':
return
subject = "Backup Upload Successful"
message = """<h3>Backup Uploaded Successfully! </h3><p>Hi there, this is just to inform you
that your backup was successfully uploaded to your Amazon S3 bucket. So relax!</p> """
else:
subject = "[Warning] Backup Upload Failed"
message = """<h3>Backup Upload Failed! </h3><p>Oops, your automated backup to Amazon S3 failed.
</p> <p>Error message: %s</p> <p>Please contact your system manager
for more information.</p>""" % error_status
if not frappe.db:
frappe.connect()
recipients = split_emails(frappe.db.get_value("S3 Backup Settings", None, "notify_email"))
frappe.sendmail(recipients=recipients, subject=subject, message=message)
send_email(False, 'Amazon S3', "S3 Backup Settings", "notify_email", error_message)
def backup_to_s3():
@ -130,11 +110,15 @@ def backup_to_s3():
endpoint_url=doc.endpoint_url or 'https://s3.amazonaws.com'
)
backup = new_backup(ignore_files=False, backup_path_db=None,
if frappe.flags.create_new_backup:
backup = new_backup(ignore_files=False, backup_path_db=None,
backup_path_files=None, backup_path_private_files=None, force=True)
db_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db))
files_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_files))
private_files = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_private_files))
db_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db))
files_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_files))
private_files = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_private_files))
else:
db_filename, files_filename, private_files = get_latest_backup_file(with_files=True)
folder = os.path.basename(db_filename)[:15] + '/'
# for adding datetime to folder name
@ -143,8 +127,8 @@ def backup_to_s3():
upload_file_to_s3(files_filename, folder, conn, bucket)
delete_old_backups(doc.backup_limit, bucket)
def upload_file_to_s3(filename, folder, conn, bucket):
def upload_file_to_s3(filename, folder, conn, bucket):
destpath = os.path.join(folder, os.path.basename(filename))
try:
print("Uploading file:", filename)
@ -156,7 +140,7 @@ def upload_file_to_s3(filename, folder, conn, bucket):
def delete_old_backups(limit, bucket):
all_backups = list()
all_backups = []
doc = frappe.get_single("S3 Backup Settings")
backup_limit = int(limit)

View file

@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import glob
import os
from frappe.utils import split_emails, get_backups_path
def send_email(success, service_name, doctype, email_field, error_status=None):
recipients = get_recipients(service_name, email_field)
if not recipients:
frappe.log_error("No Email Recipient found for {0}".format(service_name),
"{0}: Failed to send backup status email".format(service_name))
return
if success:
if not frappe.db.get_value(doctype, None, "send_email_for_successful_backup"):
return
subject = "Backup Upload Successful"
message = """
<h3>Backup Uploaded Successfully!</h3>
<p>Hi there, this is just to inform you that your backup was successfully uploaded to your {0} bucket. So relax!</p>""".format(service_name)
else:
subject = "[Warning] Backup Upload Failed"
message = """
<h3>Backup Upload Failed!</h3>
<p>Oops, your automated backup to {0} failed.</p>
<p>Error message: {1}</p>
<p>Please contact your system manager for more information.</p>""".format(service_name, error_status)
frappe.sendmail(recipients=recipients, subject=subject, message=message)
def get_recipients(service_name, email_field):
if not frappe.db:
frappe.connect()
return split_emails(frappe.db.get_value(service_name, None, email_field))
def get_latest_backup_file(with_files=False):
def get_latest(file_ext):
file_list = glob.glob(os.path.join(get_backups_path(), file_ext))
return max(file_list, key=os.path.getctime)
latest_file = get_latest('*.sql.gz')
if with_files:
latest_public_file_bak = get_latest('*-files.tar')
latest_private_file_bak = get_latest('*-private-files.tar')
return latest_file, latest_public_file_bak, latest_private_file_bak
return latest_file
def get_file_size(file_path, unit):
if not unit:
unit = 'MB'
file_size = os.path.getsize(file_path)
memory_size_unit_mapper = {'KB': 1, 'MB': 2, 'GB': 3, 'TB': 4}
i = 0
while i < memory_size_unit_mapper[unit]:
file_size = file_size / 1000.0
i += 1
return file_size
def validate_file_size():
frappe.flags.create_new_backup = True
latest_file = get_latest_backup_file()
file_size = get_file_size(latest_file, unit='GB')
if file_size > 1:
frappe.flags.create_new_backup = False

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals

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

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

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

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

@ -211,7 +211,7 @@ frappe.request.call = function(opts) {
};
if (opts.args && opts.args.doctype) {
ajax_args.headers["X-Frappe-Doctype"] = opts.args.doctype;
ajax_args.headers["X-Frappe-Doctype"] = encodeURIComponent(opts.args.doctype);
}
frappe.last_request = ajax_args.data;

View file

@ -93,6 +93,10 @@ frappe.ui.GroupBy = class {
apply_settings(settings) {
if (!settings.group_by.startsWith('`tab')) {
settings.group_by = '`tab' + this.doctype + '`.`' + settings.group_by + '`';
}
// Extract fieldname from `tabdoctype`.`fieldname`
let group_by_fieldname = settings.group_by.split('.')[1].replace(/`/g, '');
@ -160,7 +164,7 @@ frappe.ui.GroupBy = class {
if (this.aggregate_function === 'count') {
aggregate_column = 'count(`tab'+ this.doctype + '`.`name`)';
} else {
aggregate_column =
aggregate_column =
`${this.aggregate_function}(\`tab${this.aggregate_on_doctype}\`.\`${this.aggregate_on}\`)`;
aggregate_on_field = '`tab' + this.aggregate_on_doctype + '`.`' + this.aggregate_on + '`';
}
@ -252,7 +256,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

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

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

@ -197,11 +197,19 @@ class DesktopPage {
this.allow_customization = this.data.allow_customization || false;
this.allow_customization && this.make_customization_link();
!this.sections["onboarding"] &&
this.data.charts.items &&
this.make_charts();
this.data.shortcuts.items && this.make_shortcuts();
this.data.cards.items && 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();
}
if (this.allow_customization) {
// Move the widget group up to align with labels if customization is allowed
$('.desk-page .widget-group:visible:first').css('margin-top', '-25px');
@ -257,20 +265,29 @@ 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,
options: {
allow_sorting: this.allow_customization && !frappe.is_mobile(),
allow_create: this.allow_customization,
allow_delete: this.allow_customization,
allow_hiding: false,
allow_edit: true,
max_widget_count: 2,
},
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,
options: {
allow_sorting: this.allow_customization && !frappe.is_mobile(),
allow_create: this.allow_customization,
allow_delete: this.allow_customization,
allow_hiding: false,
allow_edit: true,
max_widget_count: 2,
},
widgets: this.data.charts.items
});
});
}

View file

@ -242,7 +242,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
() => this.setup_filters(),
() => this.set_route_filters(),
() => this.report_settings.onload && this.report_settings.onload(this),
() => this.get_user_settings(),
() => this.refresh()
]);
}
@ -298,6 +297,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 +363,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 +394,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();
@ -564,6 +615,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);
@ -797,13 +849,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
setTimeout(preview_chart, 500);
}
get_user_settings() {
return frappe.model.user_settings.get(this.report_name)
.then(user_settings => {
this.user_settings = user_settings;
});
}
prepare_columns(columns) {
return columns.map(column => {
column = frappe.report_utils.prepare_field_from_column(column);
@ -1058,6 +1103,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,
@ -1223,16 +1269,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: {
@ -1242,7 +1292,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();
}
});
@ -1317,11 +1368,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 => {
@ -1472,8 +1521,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

@ -174,7 +174,7 @@ export default class WebForm extends frappe.ui.FieldGroup {
title: __("Saved Successfully"),
secondary_action: () => {
if (this.success_url) {
window.location.pathname = this.success_url;
window.location.href = this.success_url;
} else if(this.login_required) {
window.location.href =
window.location.pathname + "?name=" + data.name;

View file

@ -69,6 +69,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();
if (!this.in_customize_mode) {
@ -90,7 +93,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",
@ -115,15 +118,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();
}
}
@ -139,10 +149,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 => {
@ -177,16 +187,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();
}
}
@ -236,7 +249,7 @@ export default class ChartWidget extends Widget {
}
},
{
label: __("Edit..."),
label: __("Edit"),
action: "action-edit",
handler: () => {
frappe.set_route(
@ -245,6 +258,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();
}
}
];
@ -335,6 +357,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();
}
},
@ -351,6 +374,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,
@ -407,10 +445,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);
@ -482,8 +520,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() {

View file

@ -9,6 +9,8 @@ import json
from frappe.model.document import Document
from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification,\
get_title, get_title_html
from frappe.desk.doctype.notification_settings.notification_settings\
import is_email_notifications_enabled_for_type, is_email_notifications_enabled
from frappe.utils import cint, get_fullname, getdate, get_link_to_form
class EnergyPointLog(Document):
@ -300,6 +302,10 @@ def send_summary(timespan):
if not is_energy_point_enabled():
return
if not is_email_notifications_enabled_for_type(frappe.session.user, 'Energy Point'):
return
from_date = frappe.utils.add_to_date(None, weeks=-1)
if timespan == 'Monthly':
from_date = frappe.utils.add_to_date(None, months=-1)

View file

@ -137,3 +137,29 @@ p {
position: absolute;
z-index: 2;
}
.invalid-login {
-webkit-animation: wiggle 0.5s linear;
}
@-webkit-keyframes wiggle {
8%,
41% {
-webkit-transform: translateX(-10px);
}
25%,
58% {
-webkit-transform: translateX(10px);
}
75% {
-webkit-transform: translateX(-5px);
}
92% {
-webkit-transform: translateX(5px);
}
0%,
100% {
-webkit-transform: translateX(0);
}
}

View file

@ -141,6 +141,14 @@ login.set_indicator = function(message, color) {
.removeClass().addClass('indicator').addClass(color).text(message)
}
login.set_invalid = function(message) {
$(".login-content.page-card").addClass('invalid-login');
setTimeout(() => {
$(".login-content.page-card").removeClass('invalid-login');
}, 500)
login.set_indicator(message, 'red');
}
login.login_handlers = (function() {
var get_error_handler = function(default_message) {
return function(xhr, data) {
@ -161,7 +169,7 @@ login.login_handlers = (function() {
}
if(message===default_message) {
login.set_indicator(message, 'red');
login.set_invalid(message);
} else {
login.reset_sections(false);
}

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

@ -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 == [])

View file

@ -314,6 +314,8 @@ class TestPermissions(unittest.TestCase):
frappe.set_user('Administrator')
frappe.db.sql('DELETE FROM `tabContact`')
frappe.db.sql('DELETE FROM `tabContact Email`')
frappe.db.sql('DELETE FROM `tabContact Phone`')
reset('Salutation')
reset('Contact')

Some files were not shown because too many files have changed in this diff Show more