diff --git a/frappe/__init__.py b/frappe/__init__.py index 52869be3dc..fa7af8b287 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -945,7 +945,11 @@ def get_installed_apps(sort=False, frappe_last=False): connect() if not local.all_apps: - local.all_apps = get_all_apps(True) + local.all_apps = cache().get_value('all_apps', get_all_apps) + + #cache bench apps + if not cache().get_value('all_apps'): + cache().set_value('all_apps', local.all_apps) installed = json.loads(db.get_global("installed_apps") or "[]") diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.js b/frappe/automation/doctype/auto_repeat/auto_repeat.js index 7d6bf69e3d..b57ed94e4d 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.js +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.js @@ -44,6 +44,20 @@ frappe.ui.form.on('Auto Repeat', { // auto repeat schedule frappe.auto_repeat.render_schedule(frm); + + frm.trigger('toggle_submit_on_creation'); + }, + + reference_doctype: function(frm) { + frm.trigger('toggle_submit_on_creation'); + }, + + toggle_submit_on_creation: function(frm) { + // submit on creation checkbox + frappe.model.with_doctype(frm.doc.reference_doctype, () => { + let meta = frappe.get_meta(frm.doc.reference_doctype); + frm.toggle_display('submit_on_creation', meta.is_submittable); + }); }, template: function(frm) { diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.json b/frappe/automation/doctype/auto_repeat/auto_repeat.json index 8ee6ca1d45..80975dd4f5 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.json +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "format:AUT-AR-{#####}", @@ -12,6 +13,7 @@ "section_break_3", "reference_doctype", "reference_document", + "submit_on_creation", "column_break_5", "start_date", "end_date", @@ -186,9 +188,16 @@ "fieldname": "repeat_on_last_day", "fieldtype": "Check", "label": "Repeat on Last Day of the Month" + }, + { + "default": "0", + "fieldname": "submit_on_creation", + "fieldtype": "Check", + "label": "Submit on Creation" } ], - "modified": "2019-07-17 11:30:51.412317", + "links": [], + "modified": "2020-12-10 10:43:13.449172", "modified_by": "Administrator", "module": "Automation", "name": "Auto Repeat", diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index fcf24bf1a9..31d6539e61 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -21,6 +21,7 @@ class AutoRepeat(Document): def validate(self): self.update_status() self.validate_reference_doctype() + self.validate_submit_on_creation() self.validate_dates() self.validate_email_id() self.set_dates() @@ -60,6 +61,11 @@ class AutoRepeat(Document): if not frappe.get_meta(self.reference_doctype).allow_auto_repeat: frappe.throw(_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(self.reference_doctype)) + def validate_submit_on_creation(self): + if self.submit_on_creation and not frappe.get_meta(self.reference_doctype).is_submittable: + frappe.throw(_('Cannot enable {0} for a non-submittable doctype').format( + frappe.bold('Submit on Creation'))) + def validate_dates(self): if frappe.flags.in_patch: return @@ -150,6 +156,9 @@ class AutoRepeat(Document): self.update_doc(new_doc, reference_doc) new_doc.insert(ignore_permissions = True) + if self.submit_on_creation: + new_doc.submit() + return new_doc def update_doc(self, new_doc, reference_doc): @@ -160,7 +169,7 @@ class AutoRepeat(Document): if new_doc.meta.get_field('auto_repeat'): new_doc.set('auto_repeat', self.name) - for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'remarks', 'owner']: + for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'user_remark', 'remarks', 'owner']: if new_doc.meta.get_field(fieldname): new_doc.set(fieldname, reference_doc.get(fieldname)) diff --git a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py index 60fa9cb59e..e40b12e3b9 100644 --- a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py @@ -111,6 +111,25 @@ class TestAutoRepeat(unittest.TestCase): doc = make_auto_repeat(frequency='Daily', reference_document=todo.name, start_date=add_days(today(), -2)) self.assertEqual(getdate(doc.next_schedule_date), current_date) + def test_submit_on_creation(self): + doctype = 'Test Submittable DocType' + create_submittable_doctype(doctype) + + current_date = getdate() + submittable_doc = frappe.get_doc(dict(doctype=doctype, test='test submit on creation')).insert() + submittable_doc.submit() + doc = make_auto_repeat(frequency='Daily', reference_doctype=doctype, reference_document=submittable_doc.name, + start_date=add_days(current_date, -1), submit_on_creation=1) + + data = get_auto_repeat_entries(current_date) + create_repeated_entries(data) + docnames = frappe.db.get_all(doc.reference_doctype, + filters={'auto_repeat': doc.name}, + fields=['docstatus'], + limit=1 + ) + self.assertEquals(docnames[0].docstatus, 1) + def make_auto_repeat(**args): args = frappe._dict(args) @@ -118,6 +137,7 @@ def make_auto_repeat(**args): 'doctype': 'Auto Repeat', 'reference_doctype': args.reference_doctype or 'ToDo', 'reference_document': args.reference_document or frappe.db.get_value('ToDo', 'name'), + 'submit_on_creation': args.submit_on_creation or 0, 'frequency': args.frequency or 'Daily', 'start_date': args.start_date or add_days(today(), -1), 'end_date': args.end_date or "", @@ -128,3 +148,34 @@ def make_auto_repeat(**args): }).insert(ignore_permissions=True) return doc + + +def create_submittable_doctype(doctype): + if frappe.db.exists('DocType', doctype): + return + else: + doc = frappe.get_doc({ + 'doctype': 'DocType', + '__newname': doctype, + 'module': 'Custom', + 'custom': 1, + 'is_submittable': 1, + 'fields': [{ + 'fieldname': 'test', + 'label': 'Test', + 'fieldtype': 'Data' + }], + 'permissions': [{ + 'role': 'System Manager', + 'read': 1, + 'write': 1, + 'create': 1, + 'delete': 1, + 'submit': 1, + 'cancel': 1, + 'amend': 1 + }] + }).insert() + + doc.allow_auto_repeat = 1 + doc.save() \ No newline at end of file diff --git a/frappe/boot.py b/frappe/boot.py index 8a70d4838c..dc7349676f 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -48,6 +48,7 @@ def get_bootinfo(): bootinfo.letter_heads = get_letter_heads() bootinfo.active_domains = frappe.get_active_domains() bootinfo.all_domains = [d.get("name") for d in frappe.get_all("Domain")] + add_layouts(bootinfo) bootinfo.module_app = frappe.local.module_app bootinfo.single_types = [d.name for d in frappe.get_all('DocType', {'issingle': 1})] @@ -307,6 +308,10 @@ def get_additional_filters_from_hooks(): return filter_config +def add_layouts(bootinfo): + # add routes for readable doctypes + bootinfo.doctype_layouts = frappe.get_all('DocType Layout', ['name', 'route', 'document_type']) + def get_desk_settings(): role_list = frappe.get_all('Role', fields=['*'], filters=dict( name=['in', frappe.get_roles()] diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index 47dec71fce..27e6543235 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -18,7 +18,7 @@ global_cache_keys = ("app_hooks", "installed_apps", 'scheduler_events', 'time_zone', 'webhooks', 'active_domains', 'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version', 'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts', - 'sitemap_routes', 'db_tables', 'doctype_name_map') + doctype_map_keys + 'sitemap_routes', 'db_tables') + doctype_map_keys user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang", "defaults", "user_permissions", "home_page", "linked_with", @@ -73,7 +73,7 @@ def clear_doctype_cache(doctype=None): if getattr(frappe.local, 'meta_cache') and (doctype in frappe.local.meta_cache): del frappe.local.meta_cache[doctype] - for key in ('is_table', 'doctype_modules', 'doctype_name_map', 'document_cache'): + for key in ('is_table', 'doctype_modules', 'document_cache'): cache.delete_value(key) frappe.local.document_cache = {} diff --git a/frappe/commands/site.py b/frappe/commands/site.py index bc65aa178c..4a631be3ac 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -100,13 +100,11 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas # Extract public and/or private files to the restored site, if user has given the path if with_public_files: - with_public_files = os.path.join(base_path, with_public_files) - public = extract_files(site, with_public_files, 'public') + public = extract_files(site, with_public_files) os.remove(public) if with_private_files: - with_private_files = os.path.join(base_path, with_private_files) - private = extract_files(site, with_private_files, 'private') + private = extract_files(site, with_private_files) os.remove(private) # Removing temporarily created file diff --git a/frappe/core/doctype/data_import_legacy/data_import_legacy.js b/frappe/core/doctype/data_import_legacy/data_import_legacy.js index 9a301af76e..8e4f397171 100644 --- a/frappe/core/doctype/data_import_legacy/data_import_legacy.js +++ b/frappe/core/doctype/data_import_legacy/data_import_legacy.js @@ -32,7 +32,7 @@ frappe.ui.form.on('Data Import Legacy', { frm.reload_doc(); } if (data.progress) { - let progress_bar = $(frm.dashboard.progress_area).find(".progress-bar"); + let progress_bar = $(frm.dashboard.progress_area.body).find(".progress-bar"); if (progress_bar) { $(progress_bar).removeClass("progress-bar-danger").addClass("progress-bar-success progress-bar-striped"); $(progress_bar).css("width", data.progress + "%"); diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index 215ef8cd62..569414e98b 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -132,7 +132,7 @@ "label": "Editable Grid" }, { - "default": "1", + "default": "0", "depends_on": "eval:!doc.istable && !doc.issingle", "description": "Open a dialog with mandatory fields to create a new record quickly", "fieldname": "quick_entry", @@ -427,7 +427,7 @@ "label": "Allow Guest to View" }, { - "depends_on": "has_web_view", + "depends_on": "eval:!doc.istable", "fieldname": "route", "fieldtype": "Data", "label": "Route" @@ -609,7 +609,7 @@ "link_fieldname": "reference_doctype" } ], - "modified": "2020-09-24 13:13:58.227153", + "modified": "2020-12-10 15:10:09.227205", "modified_by": "Administrator", "module": "Core", "name": "DocType", @@ -637,6 +637,7 @@ "write": 1 } ], + "route": "doctype", "search_fields": "module", "show_name_in_global_search": 1, "sort_field": "modified", diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index cce5968f9c..7c0c270277 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -26,6 +26,7 @@ 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 +from frappe.desk.utils import get_doctype_route class InvalidFieldNameError(frappe.ValidationError): pass @@ -63,15 +64,7 @@ class DocType(Document): self.validate_name() - if self.issingle: - self.allow_import = 0 - self.is_submittable = 0 - self.istable = 0 - - elif self.istable: - self.allow_import = 0 - self.permissions = [] - + self.set_defaults_for_single_and_table() self.scrub_field_names() self.set_default_in_list_view() self.set_default_translatable() @@ -79,10 +72,7 @@ class DocType(Document): self.validate_document_type() validate_fields(self) - if self.istable: - # no permission records for child table - self.permissions = [] - else: + if not self.istable: validate_permissions(self) self.make_amendable() @@ -93,8 +83,6 @@ class DocType(Document): if not self.is_new(): self.before_update = frappe.get_doc('DocType', self.name) - - if not self.is_new(): self.setup_fields_to_fetch() check_email_append_to(self) @@ -102,14 +90,20 @@ class DocType(Document): if self.default_print_format and not self.custom: frappe.throw(_('Standard DocType cannot have default print format, use Customize Form')) - if frappe.conf.get('developer_mode'): - self.owner = 'Administrator' - self.modified_by = 'Administrator' - def after_insert(self): # clear user cache so that on the next reload this doctype is included in boot clear_user_cache(frappe.session.user) + def set_defaults_for_single_and_table(self): + if self.issingle: + self.allow_import = 0 + self.is_submittable = 0 + self.istable = 0 + + elif self.istable: + self.allow_import = 0 + self.permissions = [] + 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]: @@ -134,6 +128,10 @@ 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) + if frappe.conf.get('developer_mode'): + self.owner = 'Administrator' + self.modified_by = 'Administrator' + def setup_fields_to_fetch(self): '''Setup query to update values for newly set fetch values''' try: @@ -192,6 +190,12 @@ class DocType(Document): def validate_website(self): """Ensure that website generator has field 'route'""" + if not self.istable and not self.route: + self.route = get_doctype_route(self.name) + + if self.route: + self.route = self.route.strip('/') + if self.has_web_view: # route field must be present if not 'route' in [d.fieldname for d in self.fields]: diff --git a/frappe/core/doctype/doctype/patches/set_route.py b/frappe/core/doctype/doctype/patches/set_route.py new file mode 100644 index 0000000000..655935f861 --- /dev/null +++ b/frappe/core/doctype/doctype/patches/set_route.py @@ -0,0 +1,7 @@ +import frappe +from frappe.desk.utils import get_doctype_route + +def execute(): + for doctype in frappe.get_all('DocType', ['name', 'route'], dict(istable=0)): + if not doctype.route: + frappe.db.set_value('DocType', doctype.name, 'route', get_doctype_route(doctype.name), update_modified = False) \ No newline at end of file diff --git a/frappe/core/doctype/navbar_settings/navbar_settings.py b/frappe/core/doctype/navbar_settings/navbar_settings.py index f7c437bf00..db510981a4 100644 --- a/frappe/core/doctype/navbar_settings/navbar_settings.py +++ b/frappe/core/doctype/navbar_settings/navbar_settings.py @@ -23,7 +23,7 @@ class NavbarSettings(Document): if not frappe.flags.in_patch and (len(before_save_items) > len(after_save_items)): frappe.throw(_("Please hide the standard navbar items instead of deleting them")) -@frappe.whitelist() +@frappe.whitelist(allow_guest=True) def get_app_logo(): app_logo = frappe.db.get_single_value('Navbar Settings', 'app_logo') if not app_logo: diff --git a/frappe/core/doctype/role/role.json b/frappe/core/doctype/role/role.json index 1e2366c041..e47dc7194b 100644 --- a/frappe/core/doctype/role/role.json +++ b/frappe/core/doctype/role/role.json @@ -16,7 +16,7 @@ "two_factor_auth", "navigation_settings_section", "search_bar", - "notification", + "notifications", "chat", "list_settings_section", "list_sidebar", @@ -84,12 +84,6 @@ "fieldtype": "Check", "label": "Search Bar" }, - { - "default": "1", - "fieldname": "notification", - "fieldtype": "Check", - "label": "Notification" - }, { "default": "1", "fieldname": "chat", @@ -141,13 +135,19 @@ "fieldname": "view_switcher", "fieldtype": "Check", "label": "View Switcher" + }, + { + "default": "1", + "fieldname": "notifications", + "fieldtype": "Check", + "label": "Notifications" } ], "icon": "fa fa-bookmark", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-11 17:29:13.149522", + "modified": "2020-12-03 14:08:38.181035", "modified_by": "Administrator", "module": "Core", "name": "Role", diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py index f9fbd9cbe6..bac68e30ab 100644 --- a/frappe/core/doctype/role/role.py +++ b/frappe/core/doctype/role/role.py @@ -6,7 +6,7 @@ import frappe from frappe.model.document import Document -desk_properties = ("search_bar", "notification", "chat", "list_sidebar", +desk_properties = ("search_bar", "notifications", "chat", "list_sidebar", "bulk_actions", "view_switcher", "form_sidebar", "timeline", "dashboard") class Role(Document): diff --git a/frappe/core/doctype/server_script/server_script.js b/frappe/core/doctype/server_script/server_script.js index 78ef2d0509..a317d69166 100644 --- a/frappe/core/doctype/server_script/server_script.js +++ b/frappe/core/doctype/server_script/server_script.js @@ -48,29 +48,33 @@ frappe.ui.form.on('Server Script', { setup_help(frm) { frm.get_field('help_html').html(` -
+Add logic for standard doctype events like Before Insert, After Submit, etc.
+
+
# set property
if "test" in doc.description:
- doc.status = 'Closed'
+ doc.status = 'Closed'
# validate
if "validate" in doc.description:
- raise frappe.ValidationError
+ raise frappe.ValidationError
# auto create another document
-if doc.allocted_to:
- frappe.get_doc(dict(
- doctype = 'ToDo'
- owner = doc.allocated_to,
- description = doc.subject
- )).insert()
-
+if doc.allocated_to:
+ frappe.get_doc(dict(
+ doctype = 'ToDo'
+ owner = doc.allocated_to,
+ description = doc.subject
+ )).insert()
+
+
+
Respond to /api/method/<method-name> calls, just like whitelisted methods
# respond to API
@@ -79,6 +83,21 @@ if frappe.form_dict.message == "ping":
else:
frappe.response['message'] = "ok"
+
+Add conditions to the where clause of list queries.
+
+# generate dynamic conditions and set it in the conditions variable
+tenant_id = frappe.db.get_value(...)
+conditions = 'tenant_id = {}'.format(tenant_id)
+
+# resulting select query
+select name from \`tabPerson\`
+where tenant_id = 2
+order by creation desc
+
`);
}
diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json
index 420f96ec2f..94a48f196c 100644
--- a/frappe/core/doctype/server_script/server_script.json
+++ b/frappe/core/doctype/server_script/server_script.json
@@ -24,7 +24,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Script Type",
- "options": "DocType Event\nScheduler Event\nAPI",
+ "options": "DocType Event\nScheduler Event\nPermission Query\nAPI",
"reqd": 1
},
{
@@ -35,7 +35,7 @@
"reqd": 1
},
{
- "depends_on": "eval:doc.script_type==='DocType Event'",
+ "depends_on": "eval:['DocType Event', 'Permission Query'].includes(doc.script_type)",
"fieldname": "reference_doctype",
"fieldtype": "Link",
"in_list_view": 1,
@@ -88,7 +88,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-11-11 12:39:41.391052",
+ "modified": "2020-12-03 22:42:02.708148",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",
diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py
index 839b784651..88d68dba14 100644
--- a/frappe/core/doctype/server_script/server_script.py
+++ b/frappe/core/doctype/server_script/server_script.py
@@ -4,6 +4,8 @@
from __future__ import unicode_literals
+import ast
+
import frappe
from frappe.model.document import Document
from frappe.utils.safe_exec import safe_exec
@@ -11,9 +13,9 @@ from frappe import _
class ServerScript(Document):
- @staticmethod
- def validate():
+ def validate(self):
frappe.only_for('Script Manager', True)
+ ast.parse(self.script)
@staticmethod
def on_update():
@@ -41,6 +43,12 @@ class ServerScript(Document):
# wrong report type!
raise frappe.DoesNotExistError
+ def get_permission_query_conditions(self, user):
+ locals = {"user": user, "conditions": ""}
+ safe_exec(self.script, None, locals)
+ if locals["conditions"]:
+ return locals["conditions"]
+
@frappe.whitelist()
def setup_scheduler_events(script_name, frequency):
method = frappe.scrub('{0}-{1}'.format(script_name, frequency))
diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py
index e03504f30b..4dc4f12b34 100644
--- a/frappe/core/doctype/server_script/server_script_utils.py
+++ b/frappe/core/doctype/server_script/server_script_utils.py
@@ -50,6 +50,9 @@ def get_server_script_map():
# },
# '_api': {
# '[path]': '[server script]'
+ # },
+ # 'permission_query': {
+ # 'DocType': '[server script]'
# }
# }
if frappe.flags.in_patch and not frappe.db.table_exists('Server Script'):
@@ -57,16 +60,20 @@ def get_server_script_map():
script_map = frappe.cache().get_value('server_script_map')
if script_map is None:
- script_map = {}
+ script_map = {
+ 'permission_query': {}
+ }
enabled_server_scripts = frappe.get_all('Server Script',
fields=('name', 'reference_doctype', 'doctype_event','api_method', 'script_type'),
filters={'disabled': 0})
for script in enabled_server_scripts:
if script.script_type == 'DocType Event':
script_map.setdefault(script.reference_doctype, {}).setdefault(script.doctype_event, []).append(script.name)
+ elif script.script_type == 'Permission Query':
+ script_map['permission_query'][script.reference_doctype] = script.name
else:
script_map.setdefault('_api', {})[script.api_method] = script.name
frappe.cache().set_value('server_script_map', script_map)
- return script_map
\ No newline at end of file
+ return script_map
diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py
index 3356e584af..957cbbf72d 100644
--- a/frappe/core/doctype/server_script/test_server_script.py
+++ b/frappe/core/doctype/server_script/test_server_script.py
@@ -45,6 +45,22 @@ frappe.response['message'] = 'hello'
allow_guest = 1,
script = '''
frappe.flags = 'hello'
+'''
+ ),
+ dict(
+ name='test_permission_query',
+ script_type = 'Permission Query',
+ reference_doctype = 'ToDo',
+ script = '''
+conditions = '1 = 1'
+'''),
+ dict(
+ name='test_invalid_namespace_method',
+ script_type = 'DocType Event',
+ doctype_event = 'Before Insert',
+ reference_doctype = 'Note',
+ script = '''
+frappe.method_that_doesnt_exist("do some magic")
'''
)
]
@@ -85,3 +101,12 @@ class TestServerScript(unittest.TestCase):
def test_api_return(self):
self.assertEqual(frappe.get_doc('Server Script', 'test_return_value').execute_method(), 'hello')
+
+ def test_permission_query(self):
+ self.assertTrue('where (1 = 1)' in frappe.db.get_list('ToDo', return_query=1))
+ self.assertTrue(isinstance(frappe.db.get_list('ToDo'), list))
+
+ def test_attribute_error(self):
+ """Raise AttributeError if method not found in Namespace"""
+ note = frappe.get_doc({"doctype": "Note", "title": "Test Note: Server Script"})
+ self.assertRaises(AttributeError, note.insert)
diff --git a/frappe/core/doctype/system_settings/system_settings.js b/frappe/core/doctype/system_settings/system_settings.js
index b6514dea9f..c0c9074cbc 100644
--- a/frappe/core/doctype/system_settings/system_settings.js
+++ b/frappe/core/doctype/system_settings/system_settings.js
@@ -1,37 +1,36 @@
-frappe.ui.form.on("System Settings", "refresh", function(frm) {
- frappe.call({
- method: "frappe.core.doctype.system_settings.system_settings.load",
- callback: function(data) {
- frappe.all_timezones = data.message.timezones;
- frm.set_df_property("time_zone", "options", frappe.all_timezones);
+frappe.ui.form.on("System Settings", {
+ refresh: function(frm) {
+ frappe.call({
+ method: "frappe.core.doctype.system_settings.system_settings.load",
+ callback: function(data) {
+ frappe.all_timezones = data.message.timezones;
+ frm.set_df_property("time_zone", "options", frappe.all_timezones);
- $.each(data.message.defaults, function(key, val) {
- frm.set_value(key, val);
- frappe.sys_defaults[key] = val;
- })
+ $.each(data.message.defaults, function(key, val) {
+ frm.set_value(key, val);
+ frappe.sys_defaults[key] = val;
+ });
+ }
+ });
+ },
+ enable_password_policy: function(frm) {
+ if (frm.doc.enable_password_policy == 0) {
+ frm.set_value("minimum_password_score", "");
+ } else {
+ frm.set_value("minimum_password_score", "2");
}
- });
-});
-
-frappe.ui.form.on("System Settings", "enable_password_policy", function(frm) {
- if(frm.doc.enable_password_policy == 0){
- frm.set_value("minimum_password_score", "");
- } else {
- frm.set_value("minimum_password_score", "2");
- }
-});
-
-frappe.ui.form.on("System Settings", "enable_two_factor_auth", function(frm) {
- if(frm.doc.enable_two_factor_auth == 0){
- frm.set_value("bypass_2fa_for_retricted_ip_users", 0);
- frm.set_value("bypass_restrict_ip_check_if_2fa_enabled", 0);
- }
-});
-
-frappe.ui.form.on("System Settings", "enable_prepared_report_auto_deletion", function(frm) {
- if (frm.doc.enable_prepared_report_auto_deletion) {
- if (!frm.doc.prepared_report_expiry_period) {
- frm.set_value('prepared_report_expiry_period', 7);
+ },
+ enable_two_factor_auth: function(frm) {
+ if (frm.doc.enable_two_factor_auth == 0) {
+ frm.set_value("bypass_2fa_for_retricted_ip_users", 0);
+ frm.set_value("bypass_restrict_ip_check_if_2fa_enabled", 0);
+ }
+ },
+ enable_prepared_report_auto_deletion: function(frm) {
+ if (frm.doc.enable_prepared_report_auto_deletion) {
+ if (!frm.doc.prepared_report_expiry_period) {
+ frm.set_value('prepared_report_expiry_period', 7);
+ }
}
}
});
diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py
index ba14583c2f..de14651d50 100644
--- a/frappe/core/doctype/user_permission/user_permission.py
+++ b/frappe/core/doctype/user_permission/user_permission.py
@@ -55,7 +55,7 @@ class UserPermission(Document):
ref_link = frappe.get_desk_link(self.doctype, overlap_exists[0].name)
frappe.throw(_("{0} has already assigned default value for {1}.").format(ref_link, self.allow))
-@frappe.whitelist()
+@frappe.whitelist(allow_guest=True)
def get_user_permissions(user=None):
'''Get all users permissions for the user as a dict of doctype'''
# if this is called from client-side,
@@ -66,7 +66,7 @@ def get_user_permissions(user=None):
if not user:
user = frappe.session.user
- if not user or user == "Administrator":
+ if not user or user in ("Administrator", "Guest"):
return {}
cached_user_permissions = frappe.cache().hget("user_permissions", user)
diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js
index 2d220b864c..17343573ed 100644
--- a/frappe/custom/doctype/customize_form/customize_form.js
+++ b/frappe/custom/doctype/customize_form/customize_form.js
@@ -81,6 +81,11 @@ frappe.ui.form.on("Customize Form", {
} else {
f._sortable = false;
}
+ if (f.fieldtype == "Table") {
+ frm.add_custom_button(f.options, function() {
+ frm.set_value('doc_type', f.options);
+ }, __('Customize Child Table'));
+ }
});
frm.fields_dict.fields.grid.refresh();
},
diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.json b/frappe/custom/doctype/doctype_layout/doctype_layout.json
index 420ce09a99..e47c9e03e0 100644
--- a/frappe/custom/doctype/doctype_layout/doctype_layout.json
+++ b/frappe/custom/doctype/doctype_layout/doctype_layout.json
@@ -8,6 +8,7 @@
"engine": "InnoDB",
"field_order": [
"document_type",
+ "route",
"fields",
"client_script"
],
@@ -31,11 +32,17 @@
"fieldname": "client_script",
"fieldtype": "Code",
"label": "Client Script"
+ },
+ {
+ "fieldname": "route",
+ "fieldtype": "Data",
+ "label": "Route",
+ "reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-11-17 15:49:49.669291",
+ "modified": "2020-12-10 15:01:04.352184",
"modified_by": "Administrator",
"module": "Custom",
"name": "DocType Layout",
@@ -52,8 +59,13 @@
"role": "System Manager",
"share": 1,
"write": 1
+ },
+ {
+ "read": 1,
+ "role": "Guest"
}
],
+ "route": "doctype-layout",
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.py b/frappe/custom/doctype/doctype_layout/doctype_layout.py
index a8385eaa18..b580ac8f56 100644
--- a/frappe/custom/doctype/doctype_layout/doctype_layout.py
+++ b/frappe/custom/doctype/doctype_layout/doctype_layout.py
@@ -7,6 +7,9 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
+from frappe.desk.utils import get_doctype_route
+
class DocTypeLayout(Document):
def validate(self):
- frappe.cache().delete_value('doctype_name_map')
+ if not self.route:
+ self.route = get_doctype_route(self.name)
diff --git a/frappe/custom/doctype/doctype_layout/patches/convert_web_forms_to_doctype_layout.py b/frappe/custom/doctype/doctype_layout/patches/convert_web_forms_to_doctype_layout.py
new file mode 100644
index 0000000000..4e44743b48
--- /dev/null
+++ b/frappe/custom/doctype/doctype_layout/patches/convert_web_forms_to_doctype_layout.py
@@ -0,0 +1,13 @@
+import frappe
+
+def execute():
+ for web_form_name in frappe.db.get_all('Web Form', pluck='name'):
+ web_form = frappe.get_doc('Web Form', web_form_name)
+ doctype_layout = frappe.get_doc(dict(
+ doctype = 'DocType Layout',
+ document_type = web_form.doc_type,
+ name = web_form.title,
+ route = web_form.route,
+ fields = [dict(fieldname = d.fieldname, label=d.label) for d in web_form.web_form_fields if d.fieldname]
+ )).insert()
+ print(doctype_layout.name)
\ No newline at end of file
diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql
index 15b0bed699..a52efd01e3 100644
--- a/frappe/database/mariadb/framework_mariadb.sql
+++ b/frappe/database/mariadb/framework_mariadb.sql
@@ -233,7 +233,7 @@ CREATE TABLE `tabDocType` (
DROP TABLE IF EXISTS `tabSeries`;
CREATE TABLE `tabSeries` (
- `name` varchar(100) DEFAULT NULL,
+ `name` varchar(100),
`current` int(10) NOT NULL DEFAULT 0,
PRIMARY KEY(`name`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index 3550e6167a..cc8a046363 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -162,7 +162,7 @@ class Workspace:
item_type = item_type.lower()
if item_type == "doctype":
- return (name in self.can_read and name in self.restricted_doctypes)
+ return (name in self.can_read or [] and name in self.restricted_doctypes or [])
if item_type == "page":
return (name in self.allowed_pages and name in self.restricted_pages)
if item_type == "report":
diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py
index 54bbc61e25..4e66318769 100644
--- a/frappe/desk/doctype/dashboard/dashboard.py
+++ b/frappe/desk/doctype/dashboard/dashboard.py
@@ -5,6 +5,7 @@
from __future__ import unicode_literals
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
+from frappe.config import get_modules_from_all_apps_for_user
import frappe
from frappe import _
import json
@@ -42,6 +43,24 @@ class Dashboard(Document):
except ValueError as error:
frappe.throw(_("Invalid json added in the custom options: {0}").format(error))
+
+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_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()]
+ module_condition = '`tabDashboard`.`module` in ({allowed_modules}) or `tabDashboard`.`module` is NULL'.format(
+ allowed_modules=','.join(allowed_modules))
+
+ return module_condition
+
@frappe.whitelist()
def get_permitted_charts(dashboard_name):
permitted_charts = []
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
index 3f8d7c3c79..2fa36b5514 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
@@ -13,12 +13,12 @@ from frappe.utils.dateutils import\
get_period, get_period_beginning, get_from_date_from_timespan, get_dates_from_timegrain
from frappe.model.naming import append_number_if_name_exists
from frappe.boot import get_allowed_reports
+from frappe.config import get_modules_from_all_apps_for_user
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
def get_permission_query_conditions(user):
-
if not user:
user = frappe.session.user
@@ -31,9 +31,11 @@ def get_permission_query_conditions(user):
doctype_condition = False
report_condition = False
+ module_condition = False
allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()]
allowed_reports = [frappe.db.escape(key) if type(key) == str else key.encode('UTF8') for key in get_allowed_reports()]
+ allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()]
if allowed_doctypes:
doctype_condition = '`tabDashboard Chart`.`document_type` in ({allowed_doctypes})'.format(
@@ -41,18 +43,24 @@ def get_permission_query_conditions(user):
if allowed_reports:
report_condition = '`tabDashboard Chart`.`report_name` in ({allowed_reports})'.format(
allowed_reports=','.join(allowed_reports))
+ if allowed_modules:
+ module_condition = '''`tabDashboard Chart`.`module` in ({allowed_modules})
+ or `tabDashboard Chart`.`module` is NULL'''.format(
+ allowed_modules=','.join(allowed_modules))
return '''
- (`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average')
- and {doctype_condition})
- or
- (`tabDashboard Chart`.`chart_type` = 'Report'
- and {report_condition})
- '''.format(
- doctype_condition=doctype_condition,
- report_condition=report_condition
- )
-
+ ((`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average')
+ and {doctype_condition})
+ or
+ (`tabDashboard Chart`.`chart_type` = 'Report'
+ and {report_condition}))
+ and
+ ({module_condition})
+ '''.format(
+ doctype_condition=doctype_condition,
+ report_condition=report_condition,
+ module_condition=module_condition
+ )
def has_permission(doc, ptype, user):
roles = frappe.get_roles(user)
diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py
index d4a2b00c57..6bddd09fc7 100644
--- a/frappe/desk/doctype/number_card/number_card.py
+++ b/frappe/desk/doctype/number_card/number_card.py
@@ -8,6 +8,7 @@ from frappe.model.document import Document
from frappe.utils import cint
from frappe.model.naming import append_number_if_name_exists
from frappe.modules.export_file import export_to_files
+from frappe.config import get_modules_from_all_apps_for_user
class NumberCard(Document):
def autoname(self):
@@ -33,16 +34,24 @@ def get_permission_query_conditions(user=None):
return None
doctype_condition = False
+ module_condition = False
allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()]
+ allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()]
if allowed_doctypes:
doctype_condition = '`tabNumber Card`.`document_type` in ({allowed_doctypes})'.format(
allowed_doctypes=','.join(allowed_doctypes))
+ if allowed_modules:
+ module_condition = '''`tabNumber Card`.`module` in ({allowed_modules})
+ or `tabNumber Card`.`module` is NULL'''.format(
+ allowed_modules=','.join(allowed_modules))
return '''
- {doctype_condition}
- '''.format(doctype_condition=doctype_condition)
+ {doctype_condition}
+ and
+ {module_condition}
+ '''.format(doctype_condition=doctype_condition, module_condition=module_condition)
def has_permission(doc, ptype, user):
roles = frappe.get_roles(user)
diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py
index 2d9223edf8..1f5c437330 100644
--- a/frappe/desk/form/load.py
+++ b/frappe/desk/form/load.py
@@ -13,7 +13,7 @@ from frappe.desk.form.document_follow import is_document_followed
from frappe import _
from six.moves.urllib.parse import quote
-@frappe.whitelist()
+@frappe.whitelist(allow_guest=True)
def getdoc(doctype, name, user=None):
"""
Loads a doclist for a given document. This method is called directly from the client.
@@ -52,7 +52,7 @@ def getdoc(doctype, name, user=None):
frappe.response.docs.append(doc)
-@frappe.whitelist()
+@frappe.whitelist(allow_guest=True)
def getdoctype(doctype, with_parent=False, cached_timestamp=None):
"""load doctype"""
diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py
index c28a40657f..d5428b1da2 100644
--- a/frappe/desk/form/meta.py
+++ b/frappe/desk/form/meta.py
@@ -202,13 +202,17 @@ class FormMeta(Meta):
self.load_kanban_column_fields()
def load_kanban_column_fields(self):
- values = frappe.get_list(
- 'Kanban Board', fields=['field_name'],
- filters={'reference_doctype': self.name})
+ try:
+ values = frappe.get_list(
+ 'Kanban Board', fields=['field_name'],
+ filters={'reference_doctype': self.name})
- fields = [x['field_name'] for x in values]
- fields = list(set(fields))
- self.set("__kanban_column_fields", fields, as_value=True)
+ fields = [x['field_name'] for x in values]
+ fields = list(set(fields))
+ self.set("__kanban_column_fields", fields, as_value=True)
+ except frappe.PermissionError:
+ # no access to kanban board
+ pass
def get_code_files_via_hooks(hook, name):
code_files = []
diff --git a/frappe/desk/listview.py b/frappe/desk/listview.py
index 1d10a13930..91dc0f3ba9 100644
--- a/frappe/desk/listview.py
+++ b/frappe/desk/listview.py
@@ -4,7 +4,7 @@ from __future__ import unicode_literals
import frappe
-@frappe.whitelist()
+@frappe.whitelist(allow_guest=True)
def get_list_settings(doctype):
try:
return frappe.get_cached_doc("List View Settings", doctype)
diff --git a/frappe/desk/page/user_profile/user_profile.css b/frappe/desk/page/user_profile/user_profile.css
index e69de29bb2..9bcfc3394a 100644
--- a/frappe/desk/page/user_profile/user_profile.css
+++ b/frappe/desk/page/user_profile/user_profile.css
@@ -0,0 +1,30 @@
+.recent-activity .new-timeline {
+ padding-top: 0;
+}
+
+.recent-activity .new-timeline:before {
+ top: 25px;
+}
+
+.recent-activity-title {
+ font-weight: 700;
+ font-size: var(--text-xl);
+ color: var(--text-color);
+}
+
+.recent-activity .recent-activity-footer {
+ margin-left: calc(var(--timeline-left-padding) + var(--timeline-item-left-margin));
+ max-width: var(--timeline-content-max-width);
+}
+
+.recent-activity .show-more-activity-btn {
+ display: block;
+ margin: auto;
+ width: max-content;
+ margin-top: 35px;
+ font-size: var(--text-md);
+}
+
+.recent-activity {
+ padding-bottom: 60px;
+}
\ No newline at end of file
diff --git a/frappe/desk/page/user_profile/user_profile.html b/frappe/desk/page/user_profile/user_profile.html
index b4d0fe8c22..911ccc702d 100644
--- a/frappe/desk/page/user_profile/user_profile.html
+++ b/frappe/desk/page/user_profile/user_profile.html
@@ -36,10 +36,9 @@
{%=__("Recent Activity") %}
-Order Overdue\n\nTransaction {{ name }} has exceeded Due Date. Please take necessary action.\n\nDetails\n\n- Customer: {{ customer }}\n- Amount: {{ grand_total }}\n\n\nThe fieldnames you can use in your email template are the fields in the document from which you are sending the email. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Sales Invoice)
\n\nTemplates are compiled using the Jinja Templating Language. To learn more about Jinja, read this documentation.
\n" + }, + { + "default": "0", + "fieldname": "use_html", + "fieldtype": "Check", + "label": "Use HTML" + }, + { + "depends_on": "eval:doc.use_html", + "fieldname": "response_html", + "fieldtype": "Code", + "label": "Response ", + "options": "HTML" } ], "icon": "fa fa-comment", - "modified": "2019-10-30 14:15:00.956347", + "links": [], + "modified": "2020-11-30 14:12:50.321633", "modified_by": "Administrator", "module": "Email", "name": "Email Template", diff --git a/frappe/email/doctype/email_template/email_template.py b/frappe/email/doctype/email_template/email_template.py index 2743032331..6708e9dd3f 100644 --- a/frappe/email/doctype/email_template/email_template.py +++ b/frappe/email/doctype/email_template/email_template.py @@ -9,7 +9,29 @@ from six import string_types class EmailTemplate(Document): def validate(self): - validate_template(self.response) + if self.use_html: + validate_template(self.response_html) + else: + validate_template(self.response) + + def get_formatted_subject(self, doc): + return frappe.render_template(self.subject, doc) + + def get_formatted_response(self, doc): + if self.use_html: + return frappe.render_template(self.response_html, doc) + + return frappe.render_template(self.response, doc) + + def get_formatted_email(self, doc): + if isinstance(doc, string_types): + doc = json.loads(doc) + + return { + "subject" : self.get_formatted_subject(doc), + "message" : self.get_formatted_response(doc) + } + @frappe.whitelist() def get_email_template(template_name, doc): @@ -18,5 +40,4 @@ def get_email_template(template_name, doc): doc = json.loads(doc) email_template = frappe.get_doc("Email Template", template_name) - return {"subject" : frappe.render_template(email_template.subject, doc), - "message" : frappe.render_template(email_template.response, doc)} \ No newline at end of file + return email_template.get_formatted_email(doc) \ No newline at end of file diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index a4d60706eb..2791ebb75b 100755 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -85,11 +85,11 @@ class Newsletter(WebsiteGenerator): self.db_set("scheduled_to_send", len(self.recipients)) def get_message(self): - + if self.content_type == "HTML": + return frappe.render_template(self.message_html, {"doc": self.as_dict()}) return { 'Rich Text': self.message, - 'Markdown': markdown(self.message_md), - 'HTML': self.message_html + 'Markdown': markdown(self.message_md) }[self.content_type or 'Rich Text'] def get_recipients(self): diff --git a/frappe/hooks.py b/frappe/hooks.py index 67ded8f0cf..d024cb7929 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -93,6 +93,7 @@ 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": "frappe.desk.doctype.dashboard.dashboard.get_permission_query_conditions", "Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.get_permission_query_conditions", "Number Card": "frappe.desk.doctype.number_card.number_card.get_permission_query_conditions", "Notification Settings": "frappe.desk.doctype.notification_settings.notification_settings.get_permission_query_conditions", diff --git a/frappe/installer.py b/frappe/installer.py index a365a41275..0cd5b136ae 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -440,20 +440,11 @@ def extract_sql_from_archive(sql_file_path): Returns: str: Path of the decompressed SQL file """ + from frappe.utils import get_bench_relative_path + sql_file_path = get_bench_relative_path(sql_file_path) # Extract the gzip file if user has passed *.sql.gz file instead of *.sql file - if not os.path.exists(sql_file_path): - base_path = '..' - sql_file_path = os.path.join(base_path, sql_file_path) - if not os.path.exists(sql_file_path): - print('Invalid path {0}'.format(sql_file_path[3:])) - sys.exit(1) - elif sql_file_path.startswith(os.sep): - base_path = os.sep - else: - base_path = '.' - if sql_file_path.endswith('sql.gz'): - decompressed_file_name = extract_sql_gzip(os.path.abspath(sql_file_path)) + decompressed_file_name = extract_sql_gzip(sql_file_path) else: decompressed_file_name = sql_file_path @@ -475,9 +466,12 @@ def extract_sql_gzip(sql_gz_path): return decompressed_file -def extract_files(site_name, file_path, folder_name): +def extract_files(site_name, file_path): import shutil import subprocess + from frappe.utils import get_bench_relative_path + + file_path = get_bench_relative_path(file_path) # Need to do frappe.init to maintain the site locals frappe.init(site=site_name) diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json index 123bb21e88..2ca1723cb2 100755 --- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json +++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json @@ -18,12 +18,9 @@ "bucket", "endpoint_url", "column_break_13", - "region", "backup_details_section", "frequency", - "backup_files", - "column_break_18", - "backup_limit" + "backup_files" ], "fields": [ { @@ -42,7 +39,7 @@ }, { "default": "1", - "description": "Note: By default emails for failed backups are sent.", + "description": "By default, emails are only sent for failed backups.", "fieldname": "send_email_for_successful_backup", "fieldtype": "Check", "label": "Send Email for Successful Backup" @@ -73,14 +70,7 @@ "reqd": 1 }, { - "default": "us-east-1", - "description": "See https://docs.aws.amazon.com/general/latest/gr/s3.html for details.", - "fieldname": "region", - "fieldtype": "Select", - "label": "Region", - "options": "us-east-1\nus-east-2\nus-west-1\nus-west-2\naf-south-1\nap-east-1\nap-south-1\nap-southeast-1\nap-southeast-2\nap-northeast-1\nap-northeast-2\nap-northeast-3\nca-central-1\ncn-north-1\ncn-northwest-1\neu-central-1\neu-west-1\neu-west-2\neu-west-3\neu-south-1\neu-north-1\nme-south-1\nsa-east-1" - }, - { + "default": "https://s3.amazonaws.com", "fieldname": "endpoint_url", "fieldtype": "Data", "label": "Endpoint URL" @@ -92,14 +82,6 @@ "mandatory_depends_on": "enabled", "reqd": 1 }, - { - "description": "Set to 0 for no limit on the number of backups taken", - "fieldname": "backup_limit", - "fieldtype": "Int", - "label": "Backup Limit", - "mandatory_depends_on": "enabled", - "reqd": 1 - }, { "depends_on": "enabled", "fieldname": "api_access_section", @@ -142,16 +124,12 @@ "fieldname": "backup_files", "fieldtype": "Check", "label": "Backup Files" - }, - { - "fieldname": "column_break_18", - "fieldtype": "Column Break" } ], "hide_toolbar": 1, "issingle": 1, "links": [], - "modified": "2020-07-27 17:27:21.400000", + "modified": "2020-12-07 15:30:55.047689", "modified_by": "Administrator", "module": "Integrations", "name": "S3 Backup Settings", @@ -172,4 +150,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py index 7c90d37f82..308d34c5c2 100755 --- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py +++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py @@ -24,6 +24,7 @@ class S3BackupSettings(Document): if not self.endpoint_url: self.endpoint_url = 'https://s3.amazonaws.com' + conn = boto3.client( 's3', aws_access_key_id=self.access_key_id, @@ -31,25 +32,21 @@ class S3BackupSettings(Document): endpoint_url=self.endpoint_url ) - bucket_lower = str(self.bucket) - - try: - conn.list_buckets() - - except ClientError: - frappe.throw(_("Invalid Access Key ID or Secret Access Key.")) - try: # Head_bucket returns a 200 OK if the bucket exists and have access to it. - conn.head_bucket(Bucket=bucket_lower) + # Requires ListBucket permission + conn.head_bucket(Bucket=self.bucket) except ClientError as e: error_code = e.response['Error']['Code'] + bucket_name = frappe.bold(self.bucket) if error_code == '403': - frappe.throw(_("Do not have permission to access {0} bucket.").format(bucket_lower)) - else: # '400'-Bad request or '404'-Not Found return - # try to create bucket - conn.create_bucket(Bucket=bucket_lower, CreateBucketConfiguration={ - 'LocationConstraint': self.region}) + msg = _("Do not have permission to access bucket {0}.").format(bucket_name) + elif error_code == '404': + msg = _("Bucket {0} not found.").format(bucket_name) + else: + msg = e.args[0] + + frappe.throw(msg) @frappe.whitelist() @@ -70,11 +67,13 @@ 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: @@ -146,42 +145,13 @@ def backup_to_s3(): if files_filename: 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): destpath = os.path.join(folder, os.path.basename(filename)) try: print("Uploading file:", filename) - conn.upload_file(filename, bucket, destpath) + conn.upload_file(filename, bucket, destpath) # Requires PutObject permission except Exception as e: frappe.log_error() print("Error uploading: %s" % (e)) - - -def delete_old_backups(limit, bucket): - all_backups = [] - doc = frappe.get_single("S3 Backup Settings") - backup_limit = int(limit) - - s3 = boto3.resource( - 's3', - aws_access_key_id=doc.access_key_id, - aws_secret_access_key=doc.get_password('secret_access_key'), - endpoint_url=doc.endpoint_url or 'https://s3.amazonaws.com' - ) - - bucket = s3.Bucket(bucket) - objects = bucket.meta.client.list_objects_v2(Bucket=bucket.name, Delimiter='/') - if objects: - for obj in objects.get('CommonPrefixes'): - all_backups.append(obj.get('Prefix')) - - oldest_backup = sorted(all_backups)[0] if all_backups else '' - - if len(all_backups) > backup_limit: - print("Deleting Backup: {0}".format(oldest_backup)) - for obj in bucket.objects.filter(Prefix=oldest_backup): - # delete all keys that are inside the oldest_backup - s3.Object(bucket.name, obj.key).delete() diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 0a219b4253..5d86b3bac8 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -802,12 +802,12 @@ class BaseDocument(object): if translated: val = _(val) - if absolute_value and isinstance(val, (int, float)): - val = abs(self.get(fieldname)) - if not doc: doc = getattr(self, "parent_doc", None) or self + if (absolute_value or doc.get('absolute_value')) and isinstance(val, (int, float)): + val = abs(self.get(fieldname)) + return format_value(val, df=df, doc=doc, currency=currency) def is_print_hide(self, fieldname, df=None, for_print=True): diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index ace9b04cec..ee4b1dde2a 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -18,6 +18,7 @@ from frappe.client import check_parent_permission from frappe.model.utils.user_settings import get_user_settings, update_user_settings from frappe.utils import flt, cint, get_time, make_filter_tuple, get_filter, add_to_date, cstr, get_timespan_date_range from frappe.model.meta import get_table_columns +from frappe.core.doctype.server_script.server_script_utils import get_server_script_map class DatabaseQuery(object): def __init__(self, doctype, user=None): @@ -683,15 +684,23 @@ class DatabaseQuery(object): self.match_filters.append(match_filters) def get_permission_query_conditions(self): + conditions = [] condition_methods = frappe.get_hooks("permission_query_conditions", {}).get(self.doctype, []) if condition_methods: - conditions = [] for method in condition_methods: c = frappe.call(frappe.get_attr(method), self.user) if c: conditions.append(c) - return " and ".join(conditions) if conditions else None + permision_script_name = get_server_script_map().get("permission_query", {}).get(self.doctype) + if permision_script_name: + script = frappe.get_doc("Server Script", permision_script_name) + condition = script.get_permission_query_conditions(self.user) + if condition: + conditions.append(condition) + + return " and ".join(conditions) if conditions else "" + def run_custom_query(self, query): if '%(key)s' in query: diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 8c17a5b19b..c740d495c1 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -209,7 +209,8 @@ class Meta(Document): 'owner': _('Created By'), 'modified_by': _('Modified By'), 'creation': _('Created On'), - 'modified': _('Last Modified On') + 'modified': _('Last Modified On'), + '_assign': _('Assigned To') }.get(fieldname) or _('No Label') return label diff --git a/frappe/patches.txt b/frappe/patches.txt index c4296b549f..06e776ef23 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -22,6 +22,7 @@ execute:frappe.reload_doc('email', 'doctype', 'document_follow') execute:frappe.reload_doc('core', 'doctype', 'communication_link') #2019-10-02 execute:frappe.reload_doc('core', 'doctype', 'has_role') execute:frappe.reload_doc('core', 'doctype', 'communication') #2019-10-02 +execute:frappe.reload_doc('core', 'doctype', 'server_script') frappe.patches.v11_0.replicate_old_user_permissions frappe.patches.v11_0.reload_and_rename_view_log #2019-01-03 frappe.patches.v7_1.rename_scheduler_log_to_error_log @@ -297,7 +298,7 @@ frappe.patches.v13_0.update_duration_options frappe.patches.v13_0.replace_old_data_import # 2020-06-24 frappe.patches.v13_0.create_custom_dashboards_cards_and_charts frappe.patches.v13_0.rename_is_custom_field_in_dashboard_chart -frappe.patches.v13_0.add_standard_navbar_items +frappe.patches.v13_0.add_standard_navbar_items # 2020-12-15 frappe.patches.v13_0.generate_theme_files_in_public_folder frappe.patches.v13_0.increase_password_length frappe.patches.v12_0.fix_email_id_formatting @@ -317,5 +318,7 @@ frappe.patches.v13_0.remove_custom_link execute:frappe.delete_doc("DocType", "Footer Item") frappe.patches.v13_0.replace_field_target_with_open_in_new_tab frappe.core.doctype.role.patches.v13_set_default_desk_properties +frappe.patches.v13_0.add_switch_theme_to_navbar_settings +frappe.patches.v13_0.update_icons_in_customized_desk_pages frappe.patches.v13_0.rename_desk_page_to_workspace -frappe.patches.v13_0.cleanup_desk_cards \ No newline at end of file +frappe.patches.v13_0.cleanup_desk_cards diff --git a/frappe/patches/v13_0/add_switch_theme_to_navbar_settings.py b/frappe/patches/v13_0/add_switch_theme_to_navbar_settings.py new file mode 100644 index 0000000000..29b99464b5 --- /dev/null +++ b/frappe/patches/v13_0/add_switch_theme_to_navbar_settings.py @@ -0,0 +1,21 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + navbar_settings = frappe.get_single("Navbar Settings") + + if frappe.db.exists('Navbar Item', {'item_label': 'Toggle Theme'}): + return + + for navbar_item in navbar_settings.settings_dropdown[6:]: + navbar_item.idx = navbar_item.idx + 1 + + navbar_settings.append('settings_dropdown', { + 'item_label': 'Toggle Theme', + 'item_type': 'Action', + 'action': 'new frappe.ui.ThemeSwitcher().show()', + 'is_standard': 1, + 'idx': 7 + }) + + navbar_settings.save() \ No newline at end of file diff --git a/frappe/patches/v13_0/update_icons_in_customized_desk_pages.py b/frappe/patches/v13_0/update_icons_in_customized_desk_pages.py new file mode 100644 index 0000000000..da7d054682 --- /dev/null +++ b/frappe/patches/v13_0/update_icons_in_customized_desk_pages.py @@ -0,0 +1,13 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + pages = frappe.get_all("Desk Page", filters={ "is_standard": False }, fields=["name", "extends", "for_user"]) + default_icon = {} + for page in pages: + if page.extends and page.for_user: + if not default_icon.get(page.extends): + default_icon[page.extends] = frappe.db.get_value("Desk Page", page.extends, "icon") + + icon = default_icon.get(page.extends) + frappe.db.set_value("Desk Page", page.name, "icon", icon) \ No newline at end of file diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js index e6599b2496..9ef5652dda 100644 --- a/frappe/printing/doctype/print_format/print_format.js +++ b/frappe/printing/doctype/print_format/print_format.js @@ -19,6 +19,7 @@ frappe.ui.form.on("Print Format", { } frm.trigger('render_buttons'); frm.toggle_display('standard', frappe.boot.developer_mode); + frm.trigger('hide_absolute_value_field'); }, render_buttons: function (frm) { frm.page.clear_inner_toolbar(); @@ -58,5 +59,20 @@ frappe.ui.form.on("Print Format", { frm.set_value('show_section_headings', value); frm.set_value('line_breaks', value); frm.trigger('render_buttons'); + }, + doc_type: function (frm) { + frm.trigger('hide_absolute_value_field'); + }, + hide_absolute_value_field: function (frm) { + // TODO: make it work with frm.doc.doc_type + // Problem: frm isn't updated in some random cases + const doctype = locals[frm.doc.doctype][frm.doc.name]; + if (doctype) { + frappe.model.with_doctype(doctype, () => { + const meta = frappe.get_meta(doctype); + const has_int_float_currency_field = meta.fields.filter(df => in_list(['Int', 'Float', 'Currency'], df.fieldtype)); + frm.toggle_display('absolute_value', has_int_float_currency_field.length); + }); + } } -}) +}); diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json index c67c05f063..6e51ef0018 100644 --- a/frappe/printing/doctype/print_format/print_format.json +++ b/frappe/printing/doctype/print_format/print_format.json @@ -22,6 +22,7 @@ "align_labels_right", "show_section_headings", "line_breaks", + "absolute_value", "column_break_11", "font", "css_section", @@ -196,13 +197,21 @@ "fieldtype": "Check", "hidden": 1, "label": "Print Format Builder" + }, + { + "default": "0", + "depends_on": "doc_type", + "description": "If checked, negative numberic values of Currency, Quantity or Count would be shown as positive", + "fieldname": "absolute_value", + "fieldtype": "Check", + "label": "Show absolute values" } ], "icon": "fa fa-print", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-10-27 18:27:58.307070", + "modified": "2020-12-10 18:58:55.598269", "modified_by": "Administrator", "module": "Printing", "name": "Print Format", diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js index 8dfffa18c0..82cfd6adf5 100644 --- a/frappe/printing/page/print/print.js +++ b/frappe/printing/page/print/print.js @@ -453,6 +453,7 @@ frappe.ui.form.PrintView = class { display: block !important; order: 1; margin-top: auto; + padding-top: var(--padding-xl) ` ); } diff --git a/frappe/printing/page/print_format_builder/print_format_builder.js b/frappe/printing/page/print_format_builder/print_format_builder.js index 1aeda6f86c..eb87190ab5 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder.js +++ b/frappe/printing/page/print_format_builder/print_format_builder.js @@ -208,8 +208,8 @@ frappe.PrintFormatBuilder = Class.extend({ if(!this.print_heading_template) { // default print heading template this.print_heading_template = '