diff --git a/frappe/__init__.py b/frappe/__init__.py index c540652c8c..1be882ab35 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -23,7 +23,7 @@ if sys.version[0] == '2': reload(sys) sys.setdefaultencoding("utf-8") -__version__ = '11.1.4' +__version__ = '11.1.5' __title__ = "Frappe Framework" local = Local() @@ -501,6 +501,7 @@ def read_only(): retval = fn(*args, **get_newargs(fn, kwargs)) if local and hasattr(local, 'master_db'): + local.db.close() local.db = local.master_db return retval diff --git a/frappe/core/doctype/user_permission/test_user_permission.py b/frappe/core/doctype/user_permission/test_user_permission.py index 157fa44ae2..b83d103013 100644 --- a/frappe/core/doctype/user_permission/test_user_permission.py +++ b/frappe/core/doctype/user_permission/test_user_permission.py @@ -2,9 +2,84 @@ # Copyright (c) 2017, Frappe Technologies and Contributors # See license.txt from __future__ import unicode_literals +from frappe.core.doctype.user_permission.user_permission import add_user_permissions -#import frappe +import frappe import unittest class TestUserPermission(unittest.TestCase): - pass + def test_apply_to_all(self): + ''' Create User permission for User having access to all applicable Doctypes''' + user = get_user() + param = get_params(user, apply = 1) + created = add_user_permissions(param) + self.assertEquals(created, 1) + + def test_for_applicable_on_update_from_apply_to_all(self): + ''' Update User Permission from all to some applicable Doctypes''' + user = get_user() + param = get_params(user, applicable = ["Chat Room", "Chat Message"]) + create = add_user_permissions(param) + frappe.db.commit() + + removed_apply_to_all = frappe.db.exists("User Permission", get_exists_param(user)) + created_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Room")) + created_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Message")) + + self.assertIsNone(removed_apply_to_all) + self.assertIsNotNone(created_applicable_first) + self.assertIsNotNone(created_applicable_second) + self.assertEquals(create, 1) + + def test_for_apply_to_all_on_update_from_applicable(self): + ''' Update User Permission from some to all applicable Doctypes''' + user = get_user() + param = get_params(user, apply = 1) + created = add_user_permissions(param) + created_apply_to_all = frappe.db.exists("User Permission", get_exists_param(user)) + removed_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Room")) + removed_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Message")) + + + self.assertIsNotNone(created_apply_to_all) + self.assertIsNone(removed_applicable_first) + self.assertIsNone(removed_applicable_second) + self.assertEquals(created, 1) + +def get_user(): + if frappe.db.exists('User', 'test_bulk_creation_update@example.com'): + return frappe.get_doc('User', 'test_bulk_creation_update@example.com') + else: + user = frappe.new_doc('User') + user.email = 'test_bulk_creation_update@example.com' + user.first_name = 'Test_Bulk_Creation' + user.add_roles("System Manager") + return user + +def get_params(user, apply = None , applicable = None): + ''' Return param to insert ''' + param = { + "user": user.name, + "doctype":"User", + "docname":user.name + } + if apply: + param.update({"apply_to_all_doctypes": 1}) + param.update({"applicable_doctypes": []}) + if applicable: + param.update({"apply_to_all_doctypes": 0}) + param.update({"applicable_doctypes": applicable}) + return param + +def get_exists_param(user, applicable = None): + ''' param to check existing Document ''' + param = { + "user": user.name, + "allow": "User", + "for_value": user.name, + } + if applicable: + param.update({"applicable_for": applicable}) + else: + param.update({"apply_to_all_doctypes": 1}) + return param diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py index 43dff47745..4dd152b54e 100644 --- a/frappe/core/doctype/user_permission/user_permission.py +++ b/frappe/core/doctype/user_permission/user_permission.py @@ -42,6 +42,9 @@ def get_user_permissions(user=None): if not user: user = frappe.session.user + if user == "Administrator": + return {} + cached_user_permissions = frappe.cache().hget("user_permissions", user) if cached_user_permissions is not None: @@ -112,12 +115,100 @@ def get_permitted_documents(doctype): return [d.get('doc') for d in get_user_permissions().get(doctype, []) \ if d.get('doc')] +@frappe.whitelist() +def check_applicable_doc_perm(user, doctype, docname): + frappe.only_for('System Manager') + applicable = [] + doc_exists = frappe.get_all('User Permission', + fields=['name'], + filters={"user": user, + "allow": doctype, + "for_value": docname, + "apply_to_all_doctypes":1, + }, limit=1) + if doc_exists: + applicable = get_linked_doctypes(doctype).keys() + else: + data = frappe.get_all('User Permission', + fields=['applicable_for'], + filters={"user": user, + "allow": doctype, + "for_value":docname, + }) + for d in data: + applicable.append(d.applicable_for) + return applicable + + @frappe.whitelist() def clear_user_permissions(user, for_doctype): frappe.only_for('System Manager') - total = frappe.db.count('User Permission', filters = dict(user=user, allow=for_doctype)) if total: frappe.db.sql('DELETE FROM `tabUser Permission` WHERE user=%s AND allow=%s', (user, for_doctype)) frappe.clear_cache() return total + +@frappe.whitelist() +def add_user_permissions(data): + ''' Add and update the user permissions ''' + frappe.only_for('System Manager') + if isinstance(data, frappe.string_types): + data = json.loads(data) + data = frappe._dict(data) + + d = check_applicable_doc_perm(data.user, data.doctype, data.docname) + exists = frappe.db.exists("User Permission", {"user": data.user, "allow": data.doctype, "for_value": data.docname, "apply_to_all_doctypes": 1}) + if data.apply_to_all_doctypes == 1 and not exists: + remove_applicable(d, data.user, data.doctype, data.docname) + insert_user_perm(data.user, data.doctype, data.docname, apply_to_all = 1) + return 1 + else: + remove_apply_to_all(data.user, data.doctype, data.docname) + update_applicable(d, data.applicable_doctypes, data.user, data.doctype, data.docname) + for applicable in data.applicable_doctypes : + if applicable not in d: + insert_user_perm(data.user, data.doctype, data.docname, applicable = applicable) + elif exists: + insert_user_perm(data.user, data.doctype, data.docname, applicable = applicable) + return 1 + return 0 + +def insert_user_perm(user, doctype, docname, apply_to_all=None, applicable=None): + user_perm = frappe.new_doc("User Permission") + user_perm.user = user + user_perm.allow = doctype + user_perm.for_value = docname + if applicable: + user_perm.applicable_for = applicable + user_perm.apply_to_all_doctypes = 0 + else: + user_perm.apply_to_all_doctypes = 1 + user_perm.insert() + +def remove_applicable(d, user, doctype, docname): + for applicable_for in d: + frappe.db.sql("""DELETE FROM `tabUser Permission` + WHERE `user`=%s + AND `applicable_for`=%s + AND `allow`=%s + AND `for_value`=%s + """, (user, applicable_for, doctype, docname)) + +def remove_apply_to_all(user, doctype, docname): + frappe.db.sql("""DELETE from `tabUser Permission` + WHERE `user`=%s + AND `apply_to_all_doctypes`=1 + AND `allow`=%s + AND `for_value`=%s + """,(user, doctype, docname)) + +def update_applicable(already_applied, to_apply, user, doctype, docname): + for applied in already_applied: + if applied not in to_apply: + frappe.db.sql("""DELETE FROM `tabUser Permission` + WHERE `user`=%s + AND `applicable_for`=%s + AND `allow`=%s + AND `for_value`=%s + """,(user, applied, doctype, docname)) \ No newline at end of file diff --git a/frappe/core/doctype/user_permission/user_permission_list.js b/frappe/core/doctype/user_permission/user_permission_list.js index 39a4648334..00d829b2a0 100644 --- a/frappe/core/doctype/user_permission/user_permission_list.js +++ b/frappe/core/doctype/user_permission/user_permission_list.js @@ -1,22 +1,123 @@ frappe.listview_settings['User Permission'] = { + onload: function(list_view) { - list_view.page.add_menu_item(__("Clear User Permissions"), () => { + var me = this; + list_view.page.add_inner_button( __("Add / Update"), function() { + let dialog =new frappe.ui.Dialog({ + title : __('Add User Permissions'), + fields: [ + { + fieldname: 'user', + label: __('For User'), + fieldtype: 'Link', + options: 'User', + reqd: 1, + onchange: function() { + dialog.fields_dict.doctype.set_input(undefined); + dialog.fields_dict.docname.set_input(undefined); + dialog.set_df_property("docname", "hidden", 1); + dialog.set_df_property("apply_to_all_doctypes", "hidden", 1); + dialog.set_df_property("applicable_doctypes", "hidden", 1); + } + }, + { + fieldname: 'doctype', + label: __('Document Type'), + fieldtype: 'Link', + options: 'DocType', + reqd: 1, + onchange: function() { + me.on_doctype_change(dialog); + } + }, + { + fieldname: 'docname', + label: __('Document Name'), + fieldtype: 'Dynamic Link', + options: 'doctype', + hidden: 1, + onchange: function() { + let field = dialog.fields_dict["docname"]; + if(field.value != field.last_value) { + if(dialog.fields_dict.doctype.value && dialog.fields_dict.docname.value && dialog.fields_dict.user.value){ + me.get_applicable_doctype(dialog).then(applicable => { + me.get_multi_select_options(dialog, applicable).then(options => { + me.applicable_options = options; + me.on_docname_change(dialog, options, applicable); + if(options.length > 5){ + dialog.fields_dict.applicable_doctypes.setup_select_all(); + } + }); + }); + } + } + } + }, + { + fieldname: 'apply_to_all_doctypes', + label: __('Apply to all Documents Types'), + fieldtype: 'Check', + checked: 1, + hidden: 1, + onchange: function() { + if(dialog.fields_dict.doctype.value && dialog.fields_dict.docname.value && dialog.fields_dict.user.value){ + me.on_apply_to_all_doctypes_change(dialog, me.applicable_options); + if(me.applicable_options.length > 5){ + dialog.fields_dict.applicable_doctypes.setup_select_all(); + } + } + } + }, + { + label: __("Applicable Document Types"), + fieldname: "applicable_doctypes", + fieldtype: "MultiCheck", + options: [], + columns: 2, + hidden: 1 + }, + ], + primary_action: (data) => { + data = me.validate(dialog, data); + frappe.call({ + async: false, + method: "frappe.core.doctype.user_permission.user_permission.add_user_permissions", + args: { + data : data + }, + callback: function(r) { + if(r.message === 1) { + frappe.show_alert({message:__("User Permissions created sucessfully"), indicator:'blue'}); + } else { + frappe.show_alert({message:__("Nothing to update"), indicator:'red'}); + + } + } + }); + dialog.hide(); + list_view.refresh(); + }, + primary_action_label: __('Submit') + }); + dialog.show(); + }); + list_view.page.add_inner_button( __("Bulk Delete"), function() { const dialog = new frappe.ui.Dialog({ title: __('Clear User Permissions'), fields: [ { - 'fieldname': 'user', - 'label': __('For User'), - 'fieldtype': 'Link', - 'options': 'User', - 'reqd': 1 + fieldname: 'user', + label: __('For User'), + fieldtype: 'Link', + options: 'User', + reqd: 1 }, { - 'fieldname': 'for_doctype', - 'label': __('For Document Type'), - 'fieldtype': 'Link', - 'options': 'DocType', - 'reqd': 1 + fieldname: 'for_doctype', + label: __('For Document Type'), + fieldtype: 'Link', + options: 'DocType', + reqd: 1 }, ], primary_action: (data) => { @@ -31,6 +132,8 @@ frappe.listview_settings['User Permission'] = { let message = ''; if (data === 0) { message = __('No records deleted'); + } else if(data === 1) { + message = __('{0} record deleted', [data]); } else { message = __('{0} records deleted', [data]); } @@ -43,10 +146,95 @@ frappe.listview_settings['User Permission'] = { }); }, - primary_action_label: __('Clear') + primary_action_label: __('Delete') }); dialog.show(); }); + }, + + validate: function(dialog, data) { + if(dialog.fields_dict.applicable_doctypes.get_unchecked_options().length == 0) { + data.apply_to_all_doctypes = 1; + data.applicable_doctypes = []; + return data; + } + if(data.apply_to_all_doctypes == 0 && !("applicable_doctypes" in data)) { + frappe.throw("Please select applicable Doctypes"); + } + return data; + }, + + get_applicable_doctype: function(dialog) { + return new Promise(resolve => { + frappe.call({ + method: 'frappe.core.doctype.user_permission.user_permission.check_applicable_doc_perm', + async: false, + args:{ + user: dialog.fields_dict.user.value, + doctype: dialog.fields_dict.doctype.value, + docname: dialog.fields_dict.docname.value + } + }).then(r => { + resolve(r.message); + }); + }); + }, + + get_multi_select_options: function(dialog, applicable){ + return new Promise(resolve => { + frappe.call({ + method: 'frappe.desk.form.linked_with.get_linked_doctypes', + async: false, + args:{ + user: dialog.fields_dict.user.value, + doctype: dialog.fields_dict.doctype.value, + docname: dialog.fields_dict.docname.value + } + }).then(r => { + var options = []; + for(var d in r.message){ + var checked = ($.inArray(d, applicable) != -1) ? 1 : 0; + options.push({ "label":d, "value": d , "checked": checked}); + } + resolve(options); + }); + }); + }, + + on_doctype_change: function(dialog) { + dialog.set_df_property("docname", "hidden", 0); + dialog.set_df_property("docname", "reqd", 1); + dialog.set_df_property("apply_to_all_doctypes", "hidden", 0); + dialog.set_value("apply_to_all_doctypes","checked",1); + }, + + on_docname_change: function(dialog, options, applicable) { + if(applicable.length != 0 ) { + dialog.set_primary_action("Update"); + dialog.set_title("Update User Permissions"); + dialog.set_df_property("applicable_doctypes", "options", options); + if(dialog.fields_dict.applicable_doctypes.get_checked_options().length == options.length) { + dialog.set_df_property("applicable_doctypes", "hidden", 1); + } else { + dialog.set_df_property("applicable_doctypes", "hidden", 0); + dialog.set_df_property("apply_to_all_doctypes", "checked", 0); + } + } else { + dialog.set_primary_action("Submit"); + dialog.set_title("Add User Permissions"); + dialog.set_df_property("applicable_doctypes", "options", options); + dialog.set_df_property("applicable_doctypes", "hidden", 1); + } + }, + + on_apply_to_all_doctypes_change: function(dialog, options) { + if(dialog.fields_dict.apply_to_all_doctypes.get_value() == 0) { + dialog.set_df_property("applicable_doctypes", "hidden", 0); + dialog.set_df_property("applicable_doctypes", "options", options); + } else { + dialog.set_df_property("applicable_doctypes", "options", options); + dialog.set_df_property("applicable_doctypes", "hidden", 1); + } } -}; +}; \ No newline at end of file diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index 59c783c524..eb56c458ce 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -249,6 +249,11 @@ def get_open_count(doctype, name, items=[]): :param transactions: List of transactions (json/dict) :param filters: optional filters (json/list)''' + if frappe.flags.in_migrate or frappe.flags.in_install: + return { + 'count': [] + } + frappe.has_permission(doc=frappe.get_doc(doctype, name), throw=True) meta = frappe.get_meta(doctype) diff --git a/frappe/email/doctype/email_group_member/email_group_member.py b/frappe/email/doctype/email_group_member/email_group_member.py index d0968425d0..23b279e755 100644 --- a/frappe/email/doctype/email_group_member/email_group_member.py +++ b/frappe/email/doctype/email_group_member/email_group_member.py @@ -7,7 +7,13 @@ import frappe from frappe.model.document import Document class EmailGroupMember(Document): - pass + def after_delete(self): + email_group = frappe.get_doc('Email Group', self.email_group) + email_group.update_total_subscribers() + + def after_insert(self): + email_group = frappe.get_doc('Email Group', self.email_group) + email_group.update_total_subscribers() def after_doctype_insert(): - frappe.db.add_unique("Email Group Member", ("email_group", "email")) \ No newline at end of file + frappe.db.add_unique("Email Group Member", ("email_group", "email")) diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index d69fae1f1d..9344ae1b3e 100755 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -137,7 +137,7 @@ def unsubscribe(email, name): return primary_action = frappe.utils.get_url() + "/api/method/frappe.email.doctype.newsletter.newsletter.confirmed_unsubscribe"+\ - "?" + get_signed_params({"email": email, "name":name}) + "?" + get_signed_params({"email": email, "name":name.encode('utf-8')}) return_confirmation_page(email, name, primary_action) diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index a3f43f716d..acddf36777 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -212,8 +212,15 @@ def get_context(context): please enable Allow Print For {0} in Print Settings""".format(status)), title=_("Error in Notification")) else: - return [{"print_format_attachment":1, "doctype":doc.doctype, "name": doc.name, - "print_format":self.print_format, "print_letterhead": print_settings.with_letterhead}] + return [{ + "print_format_attachment": 1, + "doctype": doc.doctype, + "name": doc.name, + "print_format": self.print_format, + "print_letterhead": print_settings.with_letterhead, + "lang": frappe.db.get_value('Print Format', self.print_format, 'default_print_language') + if self.print_format else 'en' + }] def get_template(self): diff --git a/frappe/email/queue.py b/frappe/email/queue.py index 0ed4044586..e62e7ca674 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -175,7 +175,8 @@ def get_email_queue(recipients, sender, subject, **kwargs): if att.get('fid'): _attachments.append(att) elif att.get("print_format_attachment") == 1: - att['lang'] = frappe.local.lang + if not att.get('lang', None): + att['lang'] = frappe.local.lang att['print_letterhead'] = kwargs.get('print_letterhead') _attachments.append(att) e.attachments = json.dumps(_attachments) diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py index c6bcc42f06..b48f279a5e 100644 --- a/frappe/model/create_new.py +++ b/frappe/model/create_new.py @@ -12,6 +12,7 @@ import frappe.defaults from frappe.model.db_schema import type_map import copy from frappe.core.doctype.user_permission.user_permission import get_user_permissions +from frappe.permissions import get_allowed_docs_for_doctype def get_new_doc(doctype, parent_doc = None, parentfield = None, as_dict=False): if doctype not in frappe.local.new_doc_templates: @@ -53,36 +54,39 @@ def set_user_and_static_default_values(doc): for df in doc.meta.get("fields"): if df.fieldtype in type_map: - user_default_value = get_user_default_value(df, defaults, user_permissions) + # user permissions for link options + doctype_user_permissions = user_permissions.get(df.options, []) + # Allowed records for the reference doctype (link field) + allowed_records = get_allowed_docs_for_doctype(doctype_user_permissions, df.parent) + + user_default_value = get_user_default_value(df, defaults, doctype_user_permissions, allowed_records) if user_default_value is not None: doc.set(df.fieldname, user_default_value) else: if df.fieldname != doc.meta.title_field: - static_default_value = get_static_default_value(df, user_permissions) + static_default_value = get_static_default_value(df, doctype_user_permissions, allowed_records) if static_default_value is not None: doc.set(df.fieldname, static_default_value) -def get_user_default_value(df, defaults, user_permissions): +def get_user_default_value(df, defaults, doctype_user_permissions, allowed_records): # don't set defaults for "User" link field using User Permissions! if df.fieldtype == "Link" and df.options != "User": # 1 - look in user permissions only for document_type==Setup # We don't want to include permissions of transactions to be used for defaults. - if (frappe.get_meta(df.options).document_type=="Setup" - and user_permissions_exist(df, user_permissions) - and len(user_permissions.get(df.options))==1): - return user_permissions.get(df.options)[0].get("doc") + if frappe.get_meta(df.options).document_type=="Setup" and len(allowed_records)==1: + return allowed_records[0] # 2 - Look in user defaults user_default = defaults.get(df.fieldname) - is_allowed_user_default = user_default and (not user_permissions_exist(df, user_permissions) - or (user_default in user_permissions.get(df.options, []))) + is_allowed_user_default = user_default and (not user_permissions_exist(df, doctype_user_permissions) + or user_default in allowed_records) # is this user default also allowed as per user permissions? if is_allowed_user_default: return user_default -def get_static_default_value(df, user_permissions): +def get_static_default_value(df, doctype_user_permissions, allowed_records): # 3 - look in default of docfield if df.get("default"): if df.default == "__user": @@ -93,8 +97,8 @@ def get_static_default_value(df, user_permissions): elif not df.default.startswith(":"): # a simple default value - is_allowed_default_value = (not user_permissions_exist(df, user_permissions) - or (df.default in user_permissions.get(df.options, []))) + is_allowed_default_value = (not user_permissions_exist(df, doctype_user_permissions) + or (df.default in allowed_records)) if df.fieldtype!="Link" or df.options=="User" or is_allowed_default_value: return df.default @@ -126,10 +130,10 @@ def set_dynamic_default_values(doc, parent_doc, parentfield): if parentfield: doc["parentfield"] = parentfield -def user_permissions_exist(df, user_permissions): +def user_permissions_exist(df, doctype_user_permissions): return (df.fieldtype=="Link" and not getattr(df, "ignore_user_permissions", False) - and df.options in (user_permissions or [])) + and doctype_user_permissions) def get_default_based_on_another_field(df, user_permissions, parent_doc): # default value based on another document @@ -139,7 +143,7 @@ def get_default_based_on_another_field(df, user_permissions, parent_doc): ref_fieldname = ref_doctype.lower().replace(" ", "_") reference_name = parent_doc.get(ref_fieldname) if parent_doc else frappe.db.get_default(ref_fieldname) default_value = frappe.db.get_value(ref_doctype, reference_name, df.fieldname) - is_allowed_default_value = (not user_permissions_exist(df, user_permissions) or + is_allowed_default_value = (not user_permissions_exist(df, user_permissions.get(df.options)) or (default_value in get_allowed_docs_for_doctype(user_permissions[df.options], df.parent))) # is this allowed as per user permissions diff --git a/frappe/model/document.py b/frappe/model/document.py index 6fdbf633d2..b2b68b2f62 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -166,10 +166,10 @@ class Document(BaseDocument): self.latest = frappe.get_doc(self.doctype, self.name) return self.latest - def check_permission(self, permtype='read', permlabel=None): + def check_permission(self, permtype='read', permlevel=None): """Raise `frappe.PermissionError` if not permitted""" if not self.has_permission(permtype): - self.raise_no_permission_to(permlabel or permtype) + self.raise_no_permission_to(permlevel or permtype) def has_permission(self, permtype="read", verbose=False): """Call `frappe.has_permission` if `self.flags.ignore_permissions` @@ -989,7 +989,7 @@ class Document(BaseDocument): frappe.db.commit() def db_get(self, fieldname): - '''get database vale for this fieldname''' + '''get database value for this fieldname''' return frappe.db.get_value(self.doctype, self.name, fieldname) def check_no_back_links_exist(self): diff --git a/frappe/oauth.py b/frappe/oauth.py index 0359e75214..4dc50366be 100644 --- a/frappe/oauth.py +++ b/frappe/oauth.py @@ -5,7 +5,7 @@ import pytz from frappe import _ from frappe.auth import LoginManager from oauthlib.oauth2.rfc6749.tokens import BearerToken -from oauthlib.oauth2.rfc6749.grant_types import AuthorizationCodeGrant, ImplicitGrant, ResourceOwnerPasswordCredentialsGrant, ClientCredentialsGrant, RefreshTokenGrant, OpenIDConnectAuthCode +from oauthlib.oauth2.rfc6749.grant_types import AuthorizationCodeGrant, ImplicitGrant, ResourceOwnerPasswordCredentialsGrant, ClientCredentialsGrant, RefreshTokenGrant from oauthlib.oauth2 import RequestValidator from oauthlib.oauth2.rfc6749.endpoints.authorization import AuthorizationEndpoint from oauthlib.oauth2.rfc6749.endpoints.token import TokenEndpoint @@ -40,19 +40,12 @@ class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoin implicit_grant = ImplicitGrant(request_validator) auth_grant = AuthorizationCodeGrant(request_validator) refresh_grant = RefreshTokenGrant(request_validator) - openid_connect_auth = OpenIDConnectAuthCode(request_validator) resource_owner_password_credentials_grant = ResourceOwnerPasswordCredentialsGrant(request_validator) bearer = BearerToken(request_validator, token_generator, token_expires_in, refresh_token_generator) AuthorizationEndpoint.__init__(self, default_response_type='code', response_types={ 'code': auth_grant, - 'code+token': openid_connect_auth, - 'code+id_token': openid_connect_auth, - 'code+token+id_token': openid_connect_auth, - 'code token': openid_connect_auth, - 'code id_token': openid_connect_auth, - 'code token id_token': openid_connect_auth, 'token': implicit_grant }, default_token_type=bearer) diff --git a/frappe/permissions.py b/frappe/permissions.py index 7553cb5dc5..46924331f4 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -24,8 +24,10 @@ def print_has_permission_check_logs(func): def inner(*args, **kwargs): frappe.flags['has_permission_check_logs'] = [] result = func(*args, **kwargs) + self_perm_check = True if not kwargs.get('user') else kwargs.get('user') == frappe.session.user # print only if access denied - if not result: + # and if user is checking his own permission + if not result and self_perm_check: msgprint(('
').join(frappe.flags['has_permission_check_logs'])) frappe.flags.pop('has_permission_check_logs', None) return result diff --git a/frappe/public/js/frappe/dom.js b/frappe/public/js/frappe/dom.js index d91d7e38b0..a13b8754fd 100644 --- a/frappe/public/js/frappe/dom.js +++ b/frappe/public/js/frappe/dom.js @@ -220,6 +220,16 @@ frappe.dom = { }; reader.readAsDataURL(file_obj); }); + }, + pixel_to_inches(pixels) { + const div = $('
'); + div.appendTo(document.body); + + const dpi_x = document.getElementById('dpi').offsetWidth; + const inches = pixels / dpi_x; + div.remove(); + + return inches; } } diff --git a/frappe/public/js/frappe/form/controls/multicheck.js b/frappe/public/js/frappe/form/controls/multicheck.js index 35e1a1a19b..5e8c437445 100644 --- a/frappe/public/js/frappe/form/controls/multicheck.js +++ b/frappe/public/js/frappe/form/controls/multicheck.js @@ -86,7 +86,9 @@ frappe.ui.form.ControlMultiCheck = frappe.ui.form.Control.extend({ this.selected_options.push(option_name); } else { let index = this.selected_options.indexOf(option_name); - this.selected_options.splice(index, 1); + if(index > -1) { + this.selected_options.splice(index, 1); + } } this.df.on_change && this.df.on_change(); }); diff --git a/frappe/public/js/frappe/form/print.js b/frappe/public/js/frappe/form/print.js index aa6be540ab..f0a4226444 100644 --- a/frappe/public/js/frappe/form/print.js +++ b/frappe/public/js/frappe/form/print.js @@ -136,22 +136,40 @@ frappe.ui.form.PrintPreview = Class.extend({ preview: function () { var me = this; this.get_print_html(function (out) { - me.wrapper.find(".print-format").html(out.html); + const $print_format = me.wrapper.find(".print-format"); + $print_format.html(out.html); me.show_footer(); me.set_style(out.style); + + const print_height = $print_format.get(0).offsetHeight; + const $message = me.wrapper.find(".page-break-message"); + + const print_height_inches = frappe.dom.pixel_to_inches(print_height); + // if contents are large enough, indicate that it will get printed on multiple pages + // Maximum height for an A4 document is 11.69 inches + if (print_height_inches > 11.69) { + $message.text(__('This may get printed on multiple pages')); + } else { + $message.text(''); + } }); }, show_footer: function() { // footer is hidden by default as reqd by pdf generation // simple hack to show it in print preview + this.wrapper.find('.print-format').css({ + display: 'flex', + flexDirection: 'column' + }); this.wrapper.find('.page-break').css({ 'display': 'flex', - 'flex-direction': 'column' + 'flex-direction': 'column', + 'flex': '1' }); this.wrapper.find('#footer-html').attr('style', ` display: block !important; order: 1; - margin-top: 20px; + margin-top: auto; `); }, printit: function () { diff --git a/frappe/public/js/frappe/form/templates/print_layout.html b/frappe/public/js/frappe/form/templates/print_layout.html index bbdf415b17..b2abd11099 100644 --- a/frappe/public/js/frappe/form/templates/print_layout.html +++ b/frappe/public/js/frappe/form/templates/print_layout.html @@ -31,6 +31,7 @@ +
diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 29bdbd1b92..8dd6f67209 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -137,6 +137,13 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { ); fields.forEach(f => this._add_field(f)); + + this.fields.forEach(f => { + const df = frappe.meta.get_docfield(f[1], f[0]); + if (df && df.fieldtype === 'Currency' && df.options && !df.options.includes(':')) { + this._add_field(df.options); + } + }); } patch_refresh_and_load_lib() { @@ -597,7 +604,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { args: { doctype: this.doctype, filters: this.get_filters_for_args(), - fields: [`count(distinct ${frappe.model.get_full_column_name('name', this.doctype)}) as total_count`], + fields: [`count(${frappe.model.get_full_column_name('name', this.doctype)}) as total_count`], } }).then(r => { this.total_count = r.message.values[0][0] || current_count; diff --git a/frappe/public/js/frappe/model/create_new.js b/frappe/public/js/frappe/model/create_new.js index fbbc9a24c5..2ec52feb64 100644 --- a/frappe/public/js/frappe/model/create_new.js +++ b/frappe/public/js/frappe/model/create_new.js @@ -87,10 +87,8 @@ $.extend(frappe.model, { var doctype = doc.doctype; var docfields = frappe.meta.docfield_list[doctype] || []; var updated = []; - for(var fid=0;fid { - return perm.doc === docname && (perm.applicable_for === doc.doctype || !perm.applicable_for); - }); - } + && allowed_records.length); // don't set defaults for "User" link field using User Permissions! if (df.fieldtype==="Link" && df.options!=="User") { // 1 - look in user permissions for document_type=="Setup". // We don't want to include permissions of transactions to be used for defaults. if (df.linked_document_type==="Setup" - && has_user_permissions && user_permissions[df.options].length===1) { - return user_permissions[df.options][0].doc; + && has_user_permissions && allowed_records.length===1) { + return allowed_records[0]; } if(!df.ignore_user_permissions) { @@ -165,7 +161,7 @@ $.extend(frappe.model, { } var is_allowed_user_default = user_default && - (!has_user_permissions || is_doc_allowed(df.options, user_default)); + (!has_user_permissions || allowed_records.includes(user_default)); // is this user default also allowed as per user permissions? if (is_allowed_user_default) { @@ -190,7 +186,7 @@ $.extend(frappe.model, { } else if (df["default"][0]===":") { var boot_doc = frappe.model.get_default_from_boot_docs(df, doc, parent_doc); - var is_allowed_boot_doc = !has_user_permissions || is_doc_allowed(df.options, boot_doc); + var is_allowed_boot_doc = !has_user_permissions || allowed_records.includes(boot_doc); if (is_allowed_boot_doc) { return boot_doc; @@ -201,7 +197,7 @@ $.extend(frappe.model, { } // is this default value is also allowed as per user permissions? - var is_allowed_default = !has_user_permissions || is_doc_allowed(df.options, df.default); + var is_allowed_default = !has_user_permissions || allowed_records.includes(df.default); if (df.fieldtype!=="Link" || df.options==="User" || is_allowed_default) { return df["default"]; } @@ -342,5 +338,3 @@ frappe.new_doc = function (doctype, opts, init_callback) { }); } - - diff --git a/frappe/public/js/frappe/model/perm.js b/frappe/public/js/frappe/model/perm.js index 60ae89a3da..2396962b94 100644 --- a/frappe/public/js/frappe/model/perm.js +++ b/frappe/public/js/frappe/model/perm.js @@ -151,18 +151,10 @@ $.extend(frappe.perm, { let rules = {}; let fields_to_check = frappe.meta.get_fields_to_check_permissions(doctype); $.each(fields_to_check, (i, df) => { - const user_permissions_for_doctype = user_permissions[df.options]; - // check if there are any user permission applicable for parent doctype - const has_user_permission = user_permissions_for_doctype ? user_permissions_for_doctype - .some(perm => !perm.applicable_for || perm.applicable_for === doctype) : false; - - if (has_user_permission) { - rules[df.label] = []; - user_permissions_for_doctype.map(permission => { - if (!permission.applicable_for || permission.applicable_for === doctype) { - rules[df.label].push(permission.doc); - } - }); + const user_permissions_for_doctype = user_permissions[df.options] || []; + const allowed_records = frappe.perm.get_allowed_docs_for_doctype(user_permissions_for_doctype, doctype); + if (allowed_records.length) { + rules[df.label] = allowed_records; } }); if (!$.isEmptyObject(rules)) { @@ -260,4 +252,10 @@ $.extend(frappe.perm, { return status === "None" ? false : true; }, + + get_allowed_docs_for_doctype: (user_permissions, doctype) => { + return (user_permissions || []).filter(perm => { + return (perm.applicable_for === doctype || !perm.applicable_for); + }).map(perm => perm.doc); + } }); diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index e8220bd07c..b4c550a297 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -115,10 +115,14 @@ frappe.ui.Filter = class { } update_filter_tag() { - return this._filter_value_set.then(() => { - !this.$filter_tag ? this.make_tag() : this.set_filter_button_text(); - this.filter_edit_area.hide(); - }); + if (this._filter_value_set) { + return this._filter_value_set.then(() => { + !this.$filter_tag ? this.make_tag() : this.set_filter_button_text(); + this.filter_edit_area.hide(); + }); + } else { + return Promise.resolve(); + } } remove() { diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index 371af5a514..027f1fd1b7 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -92,7 +92,7 @@ frappe.views.CommunicationComposer = Class.extend({ {label:__("Send me a copy"), fieldtype:"Check", fieldname:"send_me_a_copy", 'default': frappe.boot.user.send_me_a_copy}, {label:__("Send Read Receipt"), fieldtype:"Check", - fieldname:"send_read_receipt", default: 1}, + fieldname:"send_read_receipt"}, {label:__("Attach Document Print"), fieldtype:"Check", fieldname:"attach_document_print"}, {label:__("Select Print Format"), fieldtype:"Select", diff --git a/frappe/public/js/frappe/views/kanban/kanban_board.js b/frappe/public/js/frappe/views/kanban/kanban_board.js index 276f696189..bf777a9b7c 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_board.js +++ b/frappe/public/js/frappe/views/kanban/kanban_board.js @@ -38,7 +38,8 @@ frappe.provide("frappe.views"); cards: cards, columns: columns, cur_list: opts.cur_list, - empty_state: false + empty_state: false, + wrapper: opts.wrapper }); }, update_cards: function(updater, cards) { @@ -97,21 +98,33 @@ frappe.provide("frappe.views"); var doc_fields = {}; doc_fields[field.fieldname] = card_title; doc_fields[this.board.field_name] = column_title; - this.board.filters_array.forEach(function(f) { + this.cur_list.filter_area.get().forEach(function(f) { if (f[2] !== "=") return; doc_fields[f[1]] = f[3]; }); $.extend(doc, doc_fields); + // add the card directly + // for better ux + const card = prepare_card(doc, state); + card._disable_click = true; + const cards = [...state.cards, card]; + // remember the name which we will override later + const old_name = doc.name; + updater.set({ cards }); + if (field && !quick_entry) { return insert_doc(doc) .then(function(r) { - var updated_doc = r.message; - var card = prepare_card(doc, state, updated_doc); - var cards = state.cards.slice(); - cards.push(card); - updater.set({ cards: cards }); + // update the card in place with the updated doc + const updated_doc = r.message; + const index = state.cards.findIndex(card => card.name === old_name); + const card = prepare_card(updated_doc, state); + const new_cards = state.cards.slice(); + new_cards[index] = card; + updater.set({ cards: new_cards }); + fluxify.doAction('update_order'); }); } else { frappe.new_doc(this.doctype, doc); @@ -142,11 +155,22 @@ frappe.provide("frappe.views"); fluxify.doAction('update_card', updated_card); }); }, - update_order: function(updater, order) { + update_order: function(updater) { // cache original order const _cards = this.cards.slice(); const _columns = this.columns.slice(); + const order = {}; + this.wrapper.find('.kanban-column[data-column-value]') + .each(function() { + var col_name = $(this).data().columnValue; + order[col_name] = []; + $(this).find('.kanban-card-wrapper').each(function() { + var card_name = $(this).data().name; + order[col_name].push(card_name); + }); + }); + frappe.call({ method: method_prefix + "update_order", args: { @@ -431,17 +455,7 @@ frappe.provide("frappe.views"); wrapper.find('.kanban-card.add-card').fadeIn(100); wrapper.find('.kanban-cards').height('auto'); // update order - var order = {}; - wrapper.find('.kanban-column[data-column-value]') - .each(function() { - var col_name = $(this).data().columnValue; - order[col_name] = []; - $(this).find('.kanban-card-wrapper').each(function() { - var card_name = $(this).data().name; - order[col_name].push(card_name); - }); - }); - fluxify.doAction('update_order', order); + fluxify.doAction('update_order'); }, onAdd: function() { }, @@ -470,11 +484,11 @@ frappe.provide("frappe.views"); // not already working -- double entry e.preventDefault(); var card_title = $textarea.val(); + $new_card_area.hide(); + $textarea.val(''); fluxify.doAction('add_card', card_title, column.title) .then(() => { $btn_add.show(); - $new_card_area.hide(); - $textarea.val(''); }); } } @@ -531,7 +545,8 @@ frappe.provide("frappe.views"); function make_dom() { var opts = { name: card.name, - title: remove_img_tags(card.title) + title: remove_img_tags(card.title), + disable_click: card._disable_click ? 'disable-click' : '' }; self.$card = $(frappe.render_template('kanban_card', opts)) .appendTo(wrapper); diff --git a/frappe/public/js/frappe/views/kanban/kanban_card.html b/frappe/public/js/frappe/views/kanban/kanban_card.html index 94b6054e1c..8d749132ce 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_card.html +++ b/frappe/public/js/frappe/views/kanban/kanban_card.html @@ -1,4 +1,4 @@ -
+
{{ title }} diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 8a73bf8d83..b6dc343adc 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -639,7 +639,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } const format_cell = (value, row, column, data) => { - return frappe.format(value == null ? '' : value, column, + return frappe.format(value, column, {for_print: false, always_show_decimals: true}, data); }; @@ -992,7 +992,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { show_footer_message() { const message = __('For comparison, use >5, <10 or =324. For ranges, use 5:10 (for values between 5 & 10).'); - const execution_time_msg = __('Exection Time: {0} sec', [this.execution_time || 0.1]); + const execution_time_msg = __('Execution Time: {0} sec', [this.execution_time || 0.1]); this.page.footer.removeClass('hide').addClass('text-muted col-md-12') .html(`${message}${execution_time_msg}`); diff --git a/frappe/public/less/common.less b/frappe/public/less/common.less index 8ab34dfa6d..17c475d76f 100644 --- a/frappe/public/less/common.less +++ b/frappe/public/less/common.less @@ -304,3 +304,7 @@ a.no-decoration& { .text-small { font-size: @text-small; } + +.disable-click { + pointer-events: none; +} diff --git a/frappe/tests/test_permissions.py b/frappe/tests/test_permissions.py index 465034d7a2..32eca81fcd 100644 --- a/frappe/tests/test_permissions.py +++ b/frappe/tests/test_permissions.py @@ -82,12 +82,22 @@ class TestPermissions(unittest.TestCase): self.assertFalse("-test-blog-post" in names) def test_default_values(self): + doc = frappe.new_doc("Blog Post") + self.assertFalse(doc.get("blog_category")) + + # Fetch default based on single user permission add_user_permission("Blog Category", "_Test Blog Category 1", "test2@example.com") frappe.set_user("test2@example.com") doc = frappe.new_doc("Blog Post") self.assertEqual(doc.get("blog_category"), "_Test Blog Category 1") + # Don't fetch default if user permissions is more than 1 + add_user_permission("Blog Category", "_Test Blog Category", "test2@example.com", ignore_permissions=True) + frappe.clear_cache() + doc = frappe.new_doc("Blog Post") + self.assertFalse(doc.get("blog_category")) + def test_user_link_match_doc(self): blogger = frappe.get_doc("Blogger", "_Test Blogger 1") blogger.user = "test2@example.com" diff --git a/frappe/utils/print_format.py b/frappe/utils/print_format.py index a254f0b8d1..084f4f381c 100644 --- a/frappe/utils/print_format.py +++ b/frappe/utils/print_format.py @@ -90,13 +90,13 @@ def download_pdf(doctype, name, format=None, doc=None, no_letterhead=0): html = frappe.get_print(doctype, name, format, doc=doc, no_letterhead=no_letterhead) frappe.local.response.filename = "{name}.pdf".format(name=name.replace(" ", "-").replace("/", "-")) frappe.local.response.filecontent = get_pdf(html) - frappe.local.response.type = "download" + frappe.local.response.type = "pdf" @frappe.whitelist() def report_to_pdf(html, orientation="Landscape"): frappe.local.response.filename = "report.pdf" frappe.local.response.filecontent = get_pdf(html, {"orientation": orientation}) - frappe.local.response.type = "download" + frappe.local.response.type = "pdf" @frappe.whitelist() def print_by_server(doctype, name, print_format=None, doc=None, no_letterhead=0): diff --git a/frappe/utils/response.py b/frappe/utils/response.py index 8b19fc4607..b1f0ddb42d 100644 --- a/frappe/utils/response.py +++ b/frappe/utils/response.py @@ -42,6 +42,7 @@ def build_response(response_type=None): 'txt': as_txt, 'download': as_raw, 'json': as_json, + 'pdf': as_pdf, 'page': as_page, 'redirect': redirect, 'binary': as_binary @@ -84,6 +85,13 @@ def as_json(): response.data = json.dumps(frappe.local.response, default=json_handler, separators=(',',':')) return response +def as_pdf(): + response = Response() + response.mimetype = "application/pdf" + response.headers["Content-Disposition"] = ("filename=\"%s\"" % frappe.response['filename'].replace(' ', '_')).encode("utf-8") + response.data = frappe.response['filecontent'] + return response + def as_binary(): response = Response() response.mimetype = 'application/octet-stream' diff --git a/requirements.txt b/requirements.txt index 4c6510df51..cc94f7cc5c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,8 +34,8 @@ ndg-httpsclient pyasn1 zxcvbn-python unittest-xml-reporting -oauthlib==2.1.0 -requests-oauthlib==1.1.0 +oauthlib +requests-oauthlib pdfkit PyJWT PyPDF2