diff --git a/cypress/integration/query_report.js b/cypress/integration/query_report.js index e2a1c3fc79..9bd542977d 100644 --- a/cypress/integration/query_report.js +++ b/cypress/integration/query_report.js @@ -2,32 +2,58 @@ context('Query Report', () => { before(() => { cy.login(); cy.visit('/app/website'); + cy.insert_doc('Report', { + 'report_name': 'Test ToDo Report', + 'ref_doctype': 'ToDo', + 'report_type': 'Query Report', + 'query': 'select * from tabToDo' + }, true).as('doc'); }); it('add custom column in report', () => { cy.visit('/app/query-report/Permitted Documents For User'); cy.get('.page-form.flex', { timeout: 60000 }).should('have.length', 1).then(() => { - cy.get('#page-query-report input[data-fieldname="user"]').as('input'); - cy.get('@input').focus().type('test@erpnext.com', { delay: 100 }).blur(); + cy.get('#page-query-report input[data-fieldname="user"]').as('input-user'); + cy.get('@input-user').focus().type('test@erpnext.com', { delay: 100 }).blur(); cy.wait(300); - cy.get('#page-query-report input[data-fieldname="doctype"]').as('input-test'); - cy.get('@input-test').focus().type('Role', { delay: 100 }).blur(); + cy.get('#page-query-report input[data-fieldname="doctype"]').as('input-role'); + cy.get('@input-role').focus().type('Role', { delay: 100 }).blur(); cy.get('.datatable').should('exist'); - cy.get('.menu-btn-group button').click({ force: true }); - cy.get('.dropdown-menu li').contains('Add Column').click({ force: true }); - cy.get('.modal-dialog').should('contain', 'Add Column'); + cy.get('#page-query-report .page-actions .menu-btn-group button').click({ force: true }); + cy.get('#page-query-report .menu-btn-group .dropdown-menu').contains('Add Column').click({ force: true }); + cy.get_open_dialog().get('.modal-title').should('contain', 'Add Column'); cy.get('select[data-fieldname="doctype"]').select("Role", { force: true }); cy.get('select[data-fieldname="field"]').select("Role Name", { force: true }); cy.get('select[data-fieldname="insert_after"]').select("Name", { force: true }); - cy.get('button').contains('Submit').click({ force: true }); - cy.get('.menu-btn-group button').click({ force: true }); - cy.get('.dropdown-menu li').contains('Save').click({ force: true }); - cy.get('.modal-dialog').should('contain', 'Save Report'); + cy.get_open_dialog().findByRole('button', {name: 'Submit'}).click({ force: true }); + cy.get('#page-query-report .page-actions .menu-btn-group button').click({ force: true }); + cy.get('#page-query-report .menu-btn-group .dropdown-menu').contains('Save').click({ timeout: 100, force: true }); + cy.get_open_dialog().get('.modal-title').should('contain', 'Save Report'); cy.get('input[data-fieldname="report_name"]').type("Test Report", { delay: 100, force: true }); - cy.get('button').contains('Submit').click({ timeout: 1000, force: true }); + cy.get_open_dialog().findByRole('button', {name: 'Submit'}).click({ timeout: 1000, force: true }); }); }); + + let save_report_and_open = (report, update_name) => { + cy.get('#page-query-report .page-actions .menu-btn-group button').click({ force: true }); + cy.get('#page-query-report .menu-btn-group .dropdown-menu').contains('Save').click({ timeout: 100, force: true }); + cy.get_open_dialog().get('.modal-title').should('contain', 'Save Report'); + + cy.get('input[data-fieldname="report_name"]').type(update_name, { delay: 100, force: true }); + cy.get_open_dialog().findByRole('button', {name: 'Submit'}).click({ timeout: 1000, force: true }); + + cy.visit('/app/query-report/'+report); + cy.get('.datatable').should('exist'); + }; + + it('test multi level query report', () => { + cy.visit('/app/query-report/Test ToDo Report'); + cy.get('.datatable').should('exist'); + + save_report_and_open('Test ToDo Report 1', ' 1'); + save_report_and_open('Test ToDo Report 11', '1'); + }); }); \ No newline at end of file diff --git a/cypress/integration/sidebar.js b/cypress/integration/sidebar.js index cd771430c6..2831c9bad5 100644 --- a/cypress/integration/sidebar.js +++ b/cypress/integration/sidebar.js @@ -17,7 +17,7 @@ context('Sidebar', () => { cy.get('.group-by-item > .dropdown-item').should('contain', 'Me'); //Assigning a doctype to a user - cy.click_listview_row_item(0); + cy.visit('/app/doctype/ToDo'); cy.get('.form-assignments > .flex > .text-muted').click(); cy.get_field('assign_to_me', 'Check').click(); cy.get('.modal-footer > .standard-actions > .btn-primary').click(); @@ -44,8 +44,7 @@ context('Sidebar', () => { cy.clear_filters(); //To remove the assignment - cy.visit('/app/doctype'); - cy.click_listview_row_item(0); + cy.visit('/app/doctype/ToDo'); cy.get('.assignments > .avatar-group > .avatar > .avatar-frame').click(); cy.get('.remove-btn').click({force: true}); cy.hide_dialog(); @@ -53,4 +52,4 @@ context('Sidebar', () => { cy.click_sidebar_button("Assigned To"); cy.get('.empty-state').should('contain', 'No filters found'); }); -}); \ No newline at end of file +}); diff --git a/frappe/__init__.py b/frappe/__init__.py index 9cc83d63a4..43246a7fd6 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -487,11 +487,11 @@ def get_request_header(key, default=None): :param default: Default value.""" return request.headers.get(key, default) -def sendmail(recipients=[], sender="", subject="No Subject", message="No Message", +def sendmail(recipients=None, sender="", subject="No Subject", message="No Message", as_markdown=False, delayed=True, reference_doctype=None, reference_name=None, unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, add_unsubscribe_link=1, attachments=None, content=None, doctype=None, name=None, reply_to=None, queue_separately=False, - cc=[], bcc=[], message_id=None, in_reply_to=None, send_after=None, expose_recipients=None, + cc=None, bcc=None, message_id=None, in_reply_to=None, send_after=None, expose_recipients=None, send_priority=1, communication=None, retry=1, now=None, read_receipt=None, is_notification=False, inline_images=None, template=None, args=None, header=None, print_letterhead=False, with_container=False): """Send email using user's default **Email Account** or global default **Email Account**. @@ -521,6 +521,14 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message :param header: Append header in email :param with_container: Wraps email inside a styled container """ + + if recipients is None: + recipients = [] + if cc is None: + cc = [] + if bcc is None: + bcc = [] + text_content = None if template: message, text_content = get_email_from_template(template, args) @@ -718,18 +726,20 @@ def only_has_select_perm(doctype, user=None, ignore_permissions=False): else: return False -def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=False, throw=False): +def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=False, throw=False, parent_doctype=None): """Raises `frappe.PermissionError` if not permitted. :param doctype: DocType for which permission is to be check. :param ptype: Permission type (`read`, `write`, `create`, `submit`, `cancel`, `amend`). Default: `read`. :param doc: [optional] Checks User permissions for given doc. - :param user: [optional] Check for given user. Default: current user.""" + :param user: [optional] Check for given user. Default: current user. + :param parent_doctype: Required when checking permission for a child DocType (unless doc is specified).""" if not doctype and doc: doctype = doc.doctype import frappe.permissions - out = frappe.permissions.has_permission(doctype, ptype, doc=doc, verbose=verbose, user=user, raise_exception=throw) + out = frappe.permissions.has_permission(doctype, ptype, doc=doc, verbose=verbose, user=user, + raise_exception=throw, parent_doctype=parent_doctype) if throw and not out: if doc: frappe.throw(_("No permission for {0}").format(doc.doctype + " " + doc.name)) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 2bd3110481..27a9e86078 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -659,10 +659,14 @@ def publish_realtime(context, event, message, room, user, doctype, docname, afte @click.command('browse') @click.argument('site', required=False) +@click.option('--user', required=False, help='Login as user') @pass_context -def browse(context, site): +def browse(context, site, user=None): '''Opens the site on web browser''' + from frappe.auth import LoginManager + from frappe.auth import CookieManager import webbrowser + site = context.sites[0] if context.sites else site if not site: @@ -672,7 +676,24 @@ def browse(context, site): site = site.lower() if site in frappe.utils.get_sites(): - webbrowser.open(frappe.utils.get_site_url(site), new=2) + frappe.init(site=site) + frappe.connect() + + sid = '' + if user: + if frappe.conf.developer_mode or user == "Administrator": + frappe.utils.set_request(path="/") + frappe.local.cookie_manager = CookieManager() + frappe.local.login_manager = LoginManager() + frappe.local.login_manager.login_as(user) + sid = f'/app?sid={frappe.session.sid}' + else: + print("Please enable developer mode to login as a user") + + url = f'{frappe.utils.get_site_url(site)}{sid}' + if user == "Administrator": + print(f'Login URL: {url}') + webbrowser.open(url, new=2) else: click.echo("\nSite named \033[1m{}\033[0m doesn't exist\n".format(site)) diff --git a/frappe/contacts/address_and_contact.py b/frappe/contacts/address_and_contact.py index 79c3358665..7824568a43 100644 --- a/frappe/contacts/address_and_contact.py +++ b/frappe/contacts/address_and_contact.py @@ -16,7 +16,7 @@ def load_address_and_contact(doc, key=None): ["Dynamic Link", "link_name", "=", doc.name], ["Dynamic Link", "parenttype", "=", "Address"], ] - address_list = frappe.get_all("Address", filters=filters, fields=["*"]) + address_list = frappe.get_list("Address", filters=filters, fields=["*"]) address_list = [a.update({"display": get_address_display(a)}) for a in address_list] @@ -34,16 +34,16 @@ def load_address_and_contact(doc, key=None): ["Dynamic Link", "link_name", "=", doc.name], ["Dynamic Link", "parenttype", "=", "Contact"], ] - contact_list = frappe.get_all("Contact", filters=filters, fields=["*"]) + contact_list = frappe.get_list("Contact", filters=filters, fields=["*"]) for contact in contact_list: - contact["email_ids"] = frappe.get_list("Contact Email", filters={ + contact["email_ids"] = frappe.get_all("Contact Email", filters={ "parenttype": "Contact", "parent": contact.name, "is_primary": 0 }, fields=["email_id"]) - contact["phone_nos"] = frappe.get_list("Contact Phone", filters={ + contact["phone_nos"] = frappe.get_all("Contact Phone", filters={ "parenttype": "Contact", "parent": contact.name, "is_primary_phone": 0, diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index dfb9ff2973..9152655b85 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -262,7 +262,7 @@ def get_contact_with_phone_number(number): return contacts[0].parent if contacts else None def get_contact_name(email_id): - contact = frappe.get_list("Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1) + contact = frappe.get_all("Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1) return contact[0].parent if contact else None def get_contacts_linking_to(doctype, docname, fields=None): diff --git a/frappe/core/doctype/activity_log/activity_log.json b/frappe/core/doctype/activity_log/activity_log.json index a1ee4dafdb..ad12246a95 100644 --- a/frappe/core/doctype/activity_log/activity_log.json +++ b/frappe/core/doctype/activity_log/activity_log.json @@ -154,7 +154,7 @@ "icon": "fa fa-comment", "index_web_pages_for_search": 1, "links": [], - "modified": "2020-08-28 11:43:57.504565", + "modified": "2021-10-25 11:43:57.504565", "modified_by": "Administrator", "module": "Core", "name": "Activity Log", @@ -182,6 +182,5 @@ "sort_field": "modified", "sort_order": "DESC", "title_field": "subject", - "track_changes": 1, "track_seen": 1 } diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index db7269bbe2..f2fbc26a22 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -407,7 +407,7 @@ def get_contacts(email_strings, auto_create_contact=False): return contacts def add_contact_links_to_communication(communication, contact_name): - contact_links = frappe.get_list("Dynamic Link", filters={ + contact_links = frappe.get_all("Dynamic Link", filters={ "parenttype": "Contact", "parent": contact_name }, fields=["link_doctype", "link_name"]) diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index ef2bb02398..cd20a5c0f3 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -763,7 +763,9 @@ class Column: seen = [] fields_column_map = {} - def __init__(self, index, header, doctype, column_values, map_to_field=None, seen=[]): + def __init__(self, index, header, doctype, column_values, map_to_field=None, seen=None): + if seen is None: + seen = [] self.index = index self.column_number = index + 1 self.doctype = doctype diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index c85b4e8f67..e18edc1512 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -150,7 +150,7 @@ "fieldtype": "Column Break" }, { - "default": "1", + "default": "0", "depends_on": "eval:!doc.istable", "description": "If enabled, changes to the document are tracked and shown in timeline", "fieldname": "track_changes", @@ -649,7 +649,7 @@ "link_fieldname": "reference_doctype" } ], - "modified": "2021-09-05 15:39:13.233403", + "modified": "2021-10-29 11:39:13.233403", "modified_by": "Administrator", "module": "Core", "name": "DocType", diff --git a/frappe/core/doctype/error_log/error_log.json b/frappe/core/doctype/error_log/error_log.json index cdc7a63001..35ca3ceeef 100644 --- a/frappe/core/doctype/error_log/error_log.json +++ b/frappe/core/doctype/error_log/error_log.json @@ -112,7 +112,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-03-14 12:21:44.292471", + "modified": "2021-10-25 12:21:44.292471", "modified_by": "Administrator", "module": "Core", "name": "Error Log", @@ -144,6 +144,5 @@ "read_only_onload": 0, "show_name_in_global_search": 0, "sort_order": "ASC", - "track_changes": 1, "track_seen": 0 -} \ No newline at end of file +} diff --git a/frappe/core/doctype/error_snapshot/error_snapshot.json b/frappe/core/doctype/error_snapshot/error_snapshot.json index ea7a86d4f6..1333fe0d5b 100644 --- a/frappe/core/doctype/error_snapshot/error_snapshot.json +++ b/frappe/core/doctype/error_snapshot/error_snapshot.json @@ -359,7 +359,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2016-12-29 14:40:38.619106", + "modified": "2021-10-25 14:40:38.619106", "modified_by": "Administrator", "module": "Core", "name": "Error Snapshot", @@ -394,6 +394,5 @@ "sort_field": "timestamp", "sort_order": "DESC", "title_field": "evalue", - "track_changes": 1, "track_seen": 0 -} \ No newline at end of file +} diff --git a/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json index f86a4c8884..396b32bdf9 100644 --- a/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json +++ b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json @@ -38,7 +38,7 @@ } ], "links": [], - "modified": "2020-01-22 00:00:00.000000", + "modified": "2021-10-25 00:00:00.000000", "modified_by": "Administrator", "module": "Core", "name": "Scheduled Job Log", @@ -59,6 +59,5 @@ ], "quick_entry": 1, "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 + "sort_order": "DESC" } diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index 100e3c2790..3c091fec0b 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -69,16 +69,6 @@ frappe.method_that_doesnt_exist("do some magic") disabled = 1, script = ''' frappe.db.commit() -''' - ), - dict( - name='test_cache_methods', - script_type = 'DocType Event', - doctype_event = 'Before Save', - reference_doctype = 'ToDo', - disabled = 1, - script = ''' -frappe.cache().set_value('test_key', doc.name) ''' ) ] @@ -149,14 +139,3 @@ class TestServerScript(unittest.TestCase): server_script.disabled = 1 server_script.save() - - def test_cache_methods_in_server_script(self): - server_script = frappe.get_doc('Server Script', 'test_cache_methods') - server_script.disabled = 0 - server_script.save() - - todo = frappe.get_doc(dict(doctype='ToDo', description='test me')).insert() - self.assertEqual(todo.name, frappe.cache().get_value('test_key')) - - server_script.disabled = 1 - server_script.save() diff --git a/frappe/core/doctype/view_log/view_log.json b/frappe/core/doctype/view_log/view_log.json index 6c3247c58f..3c4486c944 100644 --- a/frappe/core/doctype/view_log/view_log.json +++ b/frappe/core/doctype/view_log/view_log.json @@ -125,7 +125,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2019-09-05 14:22:27.664645", + "modified": "2021-10-25 14:22:27.664645", "modified_by": "Administrator", "module": "Core", "name": "View Log", @@ -158,7 +158,6 @@ "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, "track_seen": 0, "track_views": 0 -} \ No newline at end of file +} diff --git a/frappe/desk/doctype/notification_log/notification_log.json b/frappe/desk/doctype/notification_log/notification_log.json index 9e802298e3..e188708277 100644 --- a/frappe/desk/doctype/notification_log/notification_log.json +++ b/frappe/desk/doctype/notification_log/notification_log.json @@ -120,7 +120,7 @@ "hide_toolbar": 1, "in_create": 1, "links": [], - "modified": "2020-09-18 17:26:09.703215", + "modified": "2021-10-25 17:26:09.703215", "modified_by": "Administrator", "module": "Desk", "name": "Notification Log", @@ -139,6 +139,5 @@ "sort_field": "modified", "sort_order": "DESC", "title_field": "subject", - "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/frappe/desk/doctype/route_history/route_history.json b/frappe/desk/doctype/route_history/route_history.json index 7390aa011b..09db2320ca 100644 --- a/frappe/desk/doctype/route_history/route_history.json +++ b/frappe/desk/doctype/route_history/route_history.json @@ -88,7 +88,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-10-05 13:26:03.106050", + "modified": "2021-10-25 13:26:03.106050", "modified_by": "Administrator", "module": "Desk", "name": "Route History", @@ -121,7 +121,6 @@ "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, "track_seen": 0, "track_views": 0 -} \ No newline at end of file +} diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py index 3aff3877d6..4550fdf0e6 100644 --- a/frappe/desk/form/linked_with.py +++ b/frappe/desk/form/linked_with.py @@ -77,7 +77,7 @@ def get_submitted_linked_docs(doctype, name, docs=None, visited=None): @frappe.whitelist() -def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=[]): +def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=None): """ Cancel all linked doctype, optionally ignore doctypes specified in a list. @@ -85,6 +85,8 @@ def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=[]): docs (json str) - It contains list of dictionaries of a linked documents. ignore_doctypes_on_cancel_all (list) - List of doctypes to ignore while cancelling. """ + if ignore_doctypes_on_cancel_all is None: + ignore_doctypes_on_cancel_all = [] docs = json.loads(docs) if isinstance(ignore_doctypes_on_cancel_all, str): @@ -96,7 +98,7 @@ def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=[]): frappe.publish_progress(percent=i/len(docs) * 100, title=_("Cancelling documents")) -def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=[]): +def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=None): """ Validate a document to be submitted and non-exempted from auto-cancel. @@ -109,7 +111,7 @@ def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=[]): """ #ignore doctype to cancel - if docinfo.get("doctype") in ignore_doctypes_on_cancel_all: + if docinfo.get("doctype") in (ignore_doctypes_on_cancel_all or []): return False # skip non-submittable doctypes since they don't need to be cancelled diff --git a/frappe/desk/link_preview.py b/frappe/desk/link_preview.py index 9b4471aa8d..03f8368a3a 100644 --- a/frappe/desk/link_preview.py +++ b/frappe/desk/link_preview.py @@ -40,6 +40,10 @@ def get_preview_data(doctype, docname): for key, val in preview_data.items(): if val and meta.has_field(key) and key not in [image_field, title_field, 'name']: - formatted_preview_data[meta.get_field(key).label] = frappe.format(val, meta.get_field(key).fieldtype) + formatted_preview_data[meta.get_field(key).label] = frappe.format( + val, + meta.get_field(key).fieldtype, + translated=True, + ) return formatted_preview_data diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index 2a4567ab4f..3fa41790b4 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -216,7 +216,7 @@ def get_filters_for(doctype): @frappe.whitelist() @frappe.read_only() -def get_open_count(doctype, name, items=[]): +def get_open_count(doctype, name, items=None): '''Get open count for given transactions and filters :param doctype: Reference DocType @@ -235,7 +235,8 @@ def get_open_count(doctype, name, items=[]): links = meta.get_dashboard_data() # compile all items in a list - if not items: + if items is None: + items = [] for group in links.transactions: items.extend(group.get("items")) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 9a37d16d0a..1e8298269f 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -59,6 +59,19 @@ def get_report_doc(report_name): return doc +def get_report_result(report, filters): + if report.report_type == "Query Report": + res = report.execute_query_report(filters) + + elif report.report_type == "Script Report": + res = report.execute_script_report(filters) + + elif report.report_type == "Custom Report": + ref_report = get_report_doc(report.report_name) + res = get_report_result(ref_report, filters) + + return res + def generate_report_result(report, filters=None, user=None, custom_columns=None): user = user or frappe.session.user filters = filters or [] @@ -66,13 +79,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None) if filters and isinstance(filters, str): filters = json.loads(filters) - res = [] - - if report.report_type == "Query Report": - res = report.execute_query_report(filters) - - elif report.report_type == "Script Report": - res = report.execute_script_report(filters) + res = get_report_result(report, filters) or [] columns, result, message, chart, report_summary, skip_total_row = ljust_list(res, 6) columns = [get_column_as_dict(col) for col in columns] diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 6c9fa2e937..fb150e4bea 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -180,15 +180,16 @@ def update_wildcard_field_param(data): def clean_params(data): - data.pop('cmd', None) - data.pop('data', None) - data.pop('ignore_permissions', None) - data.pop('view', None) - data.pop('user', None) - - if "csrf_token" in data: - del data["csrf_token"] - + for param in ( + "cmd", + "data", + "ignore_permissions", + "view", + "user", + "csrf_token", + "join" + ): + data.pop(param, None) def parse_json(data): if isinstance(data.get("filters"), str): @@ -214,11 +215,13 @@ def get_parenttype_and_fieldname(field, data): return parenttype, fieldname -def compress(data, args = {}): +def compress(data, args=None): """separate keys and values""" from frappe.desk.query_report import add_total_row if not data: return data + if args is None: + args = {} values = [] keys = list(data[0]) for row in data: @@ -423,15 +426,20 @@ def delete_bulk(doctype, items): @frappe.whitelist() @frappe.read_only() -def get_sidebar_stats(stats, doctype, filters=[]): +def get_sidebar_stats(stats, doctype, filters=None): + if filters is None: + filters = [] return {"stats": get_stats(stats, doctype, filters)} @frappe.whitelist() @frappe.read_only() -def get_stats(stats, doctype, filters=[]): +def get_stats(stats, doctype, filters=None): """get tag info""" import json + + if filters is None: + filters = [] tags = json.loads(stats) if filters: filters = json.loads(filters) @@ -480,12 +488,11 @@ def get_stats(stats, doctype, filters=[]): return stats @frappe.whitelist() -def get_filter_dashboard_data(stats, doctype, filters=[]): +def get_filter_dashboard_data(stats, doctype, filters=None): """get tags info""" import json tags = json.loads(stats) - if filters: - filters = json.loads(filters) + filters = json.loads(filters or []) stats = {} columns = frappe.db.get_table_columns(doctype) diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index 6a32ae6fd9..c25e996bd3 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -13,8 +13,8 @@ from email import policy def get_email(recipients, sender='', msg='', subject='[No Subject]', text_content = None, footer=None, print_html=None, formatted=None, attachments=None, - content=None, reply_to=None, cc=[], bcc=[], email_account=None, expose_recipients=None, - inline_images=[], header=None): + content=None, reply_to=None, cc=None, bcc=None, email_account=None, expose_recipients=None, + inline_images=None, header=None): """ Prepare an email with the following format: - multipart/mixed - multipart/alternative @@ -25,6 +25,14 @@ def get_email(recipients, sender='', msg='', subject='[No Subject]', - attachment """ content = content or msg + + if cc is None: + cc = [] + if bcc is None: + bcc = [] + if inline_images is None: + inline_images = [] + emailobj = EMail(sender, recipients, subject, reply_to=reply_to, cc=cc, bcc=bcc, email_account=email_account, expose_recipients=expose_recipients) if not content.strip().startswith("<"): diff --git a/frappe/email/receive.py b/frappe/email/receive.py index d2b4376dfd..bba31b122f 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -353,7 +353,7 @@ class EmailServer: return error_msg - def update_flag(self, folder, uid_list={}): + def update_flag(self, uid_list=None): """ set all uids mails the flag as seen """ if not uid_list: return diff --git a/frappe/frappeclient.py b/frappe/frappeclient.py index e57f82b60a..ab58979203 100644 --- a/frappe/frappeclient.py +++ b/frappe/frappeclient.py @@ -286,12 +286,16 @@ class FrappeClient(object): doc.modified = frappe.db.get_single_value(doctype, "modified") frappe.get_doc(doc).insert() - def get_api(self, method, params={}): + def get_api(self, method, params=None): + if params is None: + params = {} res = self.session.get(self.url + "/api/method/" + method + "/", params=params, verify=self.verify, headers=self.headers) return self.post_process(res) - def post_api(self, method, params={}): + def post_api(self, method, params=None): + if params is None: + params = {} res = self.session.post(self.url + "/api/method/" + method + "/", params=params, verify=self.verify, headers=self.headers) return self.post_process(res) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 1826cca9a3..066085a27c 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -81,6 +81,9 @@ class BaseDocument(object): if hasattr(self, "__setup__"): self.__setup__() + def __getitem__(self, key): + return self.get(key) if hasattr(self, key) else frappe.throw(msg=key, exc=KeyError) + @property def meta(self): if not getattr(self, "_meta", None): diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 44f1398cc7..6181832363 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -35,10 +35,10 @@ class DatabaseQuery(object): join='left join', distinct=False, start=None, page_length=None, limit=None, ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False, update=None, add_total_row=None, user_settings=None, reference_doctype=None, - run=True, strict=True, pluck=None, ignore_ddl=False) -> List: + run=True, strict=True, pluck=None, ignore_ddl=False, parent_doctype=None) -> List: if not ignore_permissions and \ - not frappe.has_permission(self.doctype, "select", user=user) and \ - not frappe.has_permission(self.doctype, "read", user=user): + not frappe.has_permission(self.doctype, "select", user=user, parent_doctype=parent_doctype) and \ + not frappe.has_permission(self.doctype, "read", user=user, parent_doctype=parent_doctype): frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(self.doctype)) raise frappe.PermissionError(self.doctype) @@ -318,7 +318,8 @@ class DatabaseQuery(object): doctype = table_name[4:-1] ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read' - if not self.flags.ignore_permissions and not frappe.has_permission(doctype, ptype=ptype): + if not self.flags.ignore_permissions and \ + not frappe.has_permission(doctype, ptype=ptype, parent_doctype=self.doctype): frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(doctype)) raise frappe.PermissionError(doctype) @@ -487,9 +488,9 @@ class DatabaseQuery(object): f.value = date_range fallback = "'0001-01-01 00:00:00'" - if (f.fieldname in ('creation', 'modified')): + if f.operator in ('>', '<') and (f.fieldname in ('creation', 'modified')): value = cstr(f.value) - fallback = "NULL" + fallback = "'0001-01-01 00:00:00'" elif f.operator.lower() in ('between') and \ (f.fieldname in ('creation', 'modified') or (df and (df.fieldtype=="Date" or df.fieldtype=="Datetime"))): @@ -544,6 +545,7 @@ class DatabaseQuery(object): fallback = 0 if isinstance(f.value, Column): + can_be_null = False # added to avoid the ifnull/coalesce addition quote = '"' if frappe.conf.db_type == 'postgres' else "`" value = f"{tname}.{quote}{f.value.name}{quote}" diff --git a/frappe/parallel_test_runner.py b/frappe/parallel_test_runner.py index c7f723bbdc..1a6892d30d 100644 --- a/frappe/parallel_test_runner.py +++ b/frappe/parallel_test_runner.py @@ -238,7 +238,9 @@ class ParallelTestWithOrchestrator(ParallelTestRunner): self.call_orchestrator('test-completed') return super().print_result() - def call_orchestrator(self, endpoint, data={}): + def call_orchestrator(self, endpoint, data=None): + if data is None: + data = {} # add repo token header # build id in header headers = { diff --git a/frappe/patches.txt b/frappe/patches.txt index 078bdcd5b4..abfd2ee31c 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -178,6 +178,7 @@ frappe.patches.v13_0.rename_list_view_setting_to_list_view_settings frappe.patches.v13_0.remove_twilio_settings frappe.patches.v12_0.rename_uploaded_files_with_proper_name frappe.patches.v13_0.queryreport_columns +execute:frappe.reload_doc('core', 'doctype', 'doctype') frappe.patches.v13_0.jinja_hook frappe.patches.v13_0.update_notification_channel_if_empty frappe.patches.v14_0.drop_data_import_legacy diff --git a/frappe/permissions.py b/frappe/permissions.py index 29651b4145..96e1910462 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -34,7 +34,7 @@ def print_has_permission_check_logs(func): return inner @print_has_permission_check_logs -def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None, raise_exception=True): +def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None, raise_exception=True, parent_doctype=None): """Returns True if user has permission `ptype` for given `doctype`. If `doc` is passed, it also checks user, share and owner permissions. @@ -47,11 +47,12 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None, ra doc = doctype doctype = doc.doctype - if frappe.is_table(doctype): + if user == "Administrator": return True - if user=="Administrator": - return True + if frappe.is_table(doctype): + return has_child_table_permission(doctype, ptype, doc, verbose, + user, raise_exception, parent_doctype) meta = frappe.get_meta(doctype) @@ -96,7 +97,7 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None, ra if not perm: perm = false_if_not_shared() - return perm + return bool(perm) def get_doc_permissions(doc, user=None, ptype=None): """Returns a dict of evaluated permissions for given `doc` like `{"read":1, "write":1}`""" @@ -560,3 +561,35 @@ def filter_allowed_docs_for_doctype(user_permissions, doctype, with_default_doc= def push_perm_check_log(log): if frappe.flags.get('has_permission_check_logs') == None: return frappe.flags.get('has_permission_check_logs').append(_(log)) + +def has_child_table_permission(child_doctype, ptype="read", child_doc=None, + verbose=False, user=None, raise_exception=True, parent_doctype=None): + parent_doc = None + + if child_doc: + parent_doctype = child_doc.get("parenttype") + parent_doc = frappe.get_cached_doc({ + "doctype": parent_doctype, + "docname": child_doc.get("parent") + }) + + if parent_doctype: + if not is_parent_valid(child_doctype, parent_doctype): + frappe.throw(_("{0} is not a valid parent DocType for {1}").format( + frappe.bold(parent_doctype), + frappe.bold(child_doctype) + ), title=_("Invalid Parent DocType")) + else: + frappe.throw(_("Please specify a valid parent DocType for {0}").format( + frappe.bold(child_doctype) + ), title=_("Parent DocType Required")) + + return has_permission(parent_doctype, ptype=ptype, doc=parent_doc, + verbose=verbose, user=user, raise_exception=raise_exception) + + +def is_parent_valid(child_doctype, parent_doctype): + from frappe.core.utils import find + parent_meta = frappe.get_meta(parent_doctype) + child_table_field_exists = find(parent_meta.get_table_fields(), lambda d: d.options == child_doctype) + return not parent_meta.istable and child_table_field_exists \ No newline at end of file diff --git a/frappe/public/js/frappe/form/controls/text_editor.js b/frappe/public/js/frappe/form/controls/text_editor.js index d1a4e3c8cb..f9ee15692c 100644 --- a/frappe/public/js/frappe/form/controls/text_editor.js +++ b/frappe/public/js/frappe/form/controls/text_editor.js @@ -55,6 +55,10 @@ Quill.register(FontStyle, true); Quill.register(AlignStyle, true); Quill.register(DirectionStyle, true); +// direction class +const DirectionClass = Quill.import('attributors/class/direction'); +Quill.register(DirectionClass, true); + // replace font tag with span const Inline = Quill.import('blots/inline'); diff --git a/frappe/public/js/frappe/ui/toolbar/search.js b/frappe/public/js/frappe/ui/toolbar/search.js index b5768e0da1..7d262a34d7 100644 --- a/frappe/public/js/frappe/ui/toolbar/search.js +++ b/frappe/public/js/frappe/ui/toolbar/search.js @@ -354,7 +354,7 @@ frappe.search.SearchDialog = class { get_link(result) { let link = ""; if (result.route) { - link = `href="#${result.route.join("/")}"`; + link = `href="/app/${result.route.join("/")}"`; } else if (result.data_path) { link = `data-path=${result.data_path}"`; } diff --git a/frappe/public/scss/website/navbar.scss b/frappe/public/scss/website/navbar.scss index 3496a8907c..d70d064d58 100644 --- a/frappe/public/scss/website/navbar.scss +++ b/frappe/public/scss/website/navbar.scss @@ -13,6 +13,10 @@ .navbar-light { border-bottom: 1px solid $border-color; background: $navbar-bg; + + .navbar-toggler .icon { + stroke: none; + } } .navbar-primary { @@ -25,6 +29,10 @@ } } + .navbar-brand { + color: white; + } + .navbar-search { background-color: var(--blue-400); width: 300px; @@ -36,6 +44,14 @@ } } + .navbar-toggler { + border-color: rgba(255,255,255, 0.1); + + .icon { + stroke: none; + } + } + svg use { --icon-stroke: white; diff --git a/frappe/social/doctype/energy_point_log/energy_point_log.json b/frappe/social/doctype/energy_point_log/energy_point_log.json index 6f5e411a47..8595783d1b 100644 --- a/frappe/social/doctype/energy_point_log/energy_point_log.json +++ b/frappe/social/doctype/energy_point_log/energy_point_log.json @@ -112,7 +112,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-10-06 17:25:40.477044", + "modified": "2021-10-25 17:25:40.477044", "modified_by": "Administrator", "module": "Social", "name": "Energy Point Log", @@ -131,6 +131,5 @@ ], "sort_field": "modified", "sort_order": "DESC", - "title_field": "user", - "track_changes": 1 -} \ No newline at end of file + "title_field": "user" +} diff --git a/frappe/templates/doc.html b/frappe/templates/doc.html index f3a8ab8cc8..c7b369633a 100644 --- a/frappe/templates/doc.html +++ b/frappe/templates/doc.html @@ -30,7 +30,11 @@ aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> - + + + + + diff --git a/frappe/templates/includes/navbar/navbar.html b/frappe/templates/includes/navbar/navbar.html index 1fb4ae9fb0..41ad55bbd6 100644 --- a/frappe/templates/includes/navbar/navbar.html +++ b/frappe/templates/includes/navbar/navbar.html @@ -15,7 +15,11 @@ aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> - + + + + +