diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 510e7c7678..dba13f9358 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -12,4 +12,4 @@ jobs: - name: curl run: | apk add curl bash - curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token ${{ secrets.TRAVIS_CI_TOKEN }}" -d '{"request":{"branch":"master"}}' https://api.travis-ci.com/repo/frappe%2Ffrappe_docker/requests + curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: Bearer ${{ secrets.CI_PAT }}" https://api.github.com/repos/frappe/frappe_docker/actions/workflows/build_stable.yml/dispatches -d '{"ref":"main"}' 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 3d3acd99e2..cf5642b452 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -137,10 +137,10 @@ lang = local("lang") if typing.TYPE_CHECKING: from frappe.database.mariadb.database import MariaDBDatabase from frappe.database.postgres.database import PostgresDatabase - from pypika import Query + from frappe.query_builder.builder import MariaDB, Postgres db: typing.Union[MariaDBDatabase, PostgresDatabase] - qb: Query + qb: typing.Union[MariaDB, Postgres] # end: static analysis hack @@ -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/access_log/access_log.py b/frappe/core/doctype/access_log/access_log.py index f631353d56..48c12fd93f 100644 --- a/frappe/core/doctype/access_log/access_log.py +++ b/frappe/core/doctype/access_log/access_log.py @@ -1,6 +1,7 @@ # Copyright (c) 2021, Frappe Technologies and contributors # License: MIT. See LICENSE import frappe +from frappe.utils import cstr from tenacity import retry, retry_if_exception_type, stop_after_attempt from frappe.model.document import Document @@ -24,25 +25,21 @@ def make_access_log( page=None, columns=None, ): - user = frappe.session.user in_request = frappe.request and frappe.request.method == "GET" - doc = frappe.get_doc( - { - "doctype": "Access Log", - "user": user, - "export_from": doctype, - "reference_document": document, - "file_type": file_type, - "report_name": report_name, - "page": page, - "method": method, - "filters": frappe.utils.cstr(filters) if filters else None, - "columns": columns, - } - ) - doc.insert(ignore_permissions=True) + frappe.get_doc({ + "doctype": "Access Log", + "user": user, + "export_from": doctype, + "reference_document": document, + "file_type": file_type, + "report_name": report_name, + "page": page, + "method": method, + "filters": cstr(filters) or None, + "columns": columns, + }).db_insert() # `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview` # dont commit in test mode 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 bd33189d58..3a78a6a599 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -406,7 +406,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/transaction_log/transaction_log.py b/frappe/core/doctype/transaction_log/transaction_log.py index e2e75b130c..79e1cc30ac 100644 --- a/frappe/core/doctype/transaction_log/transaction_log.py +++ b/frappe/core/doctype/transaction_log/transaction_log.py @@ -1,12 +1,14 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2018, Frappe Technologies and contributors +# Copyright (c) 2021, Frappe Technologies and contributors # License: MIT. See LICENSE +import hashlib + import frappe from frappe import _ from frappe.model.document import Document +from frappe.query_builder import DocType from frappe.utils import cint, now_datetime -import hashlib + class TransactionLog(Document): def before_insert(self): @@ -44,10 +46,14 @@ class TransactionLog(Document): def get_current_index(): - current = frappe.db.sql("""SELECT `current` - FROM `tabSeries` - WHERE `name` = 'TRANSACTLOG' - FOR UPDATE""") + series = DocType("Series") + current = ( + frappe.qb.from_(series) + .where(series.name == "TRANSACTLOG") + .for_update() + .select("current") + ).run() + if current and current[0][0] is not None: current = current[0][0] diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 45f7d47a27..b6f6f66ee4 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -717,8 +717,10 @@ def ask_pass_update(): # update the sys defaults as to awaiting users from frappe.utils import set_default - users = frappe.db.sql("""SELECT DISTINCT(parent) as user FROM `tabUser Email` - WHERE awaiting_password = 1""", as_dict=True) + doctype = DocType("User Email") + users = frappe.qb.from_(doctype).where(doctype.awaiting_password == 1).select( + doctype.parent.as_("user") + ).distinct().run(as_dict=True) password_list = [ user.get("user") for user in users ] set_default("email_user_password", u','.join(password_list)) 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/core/report/transaction_log_report/transaction_log_report.py b/frappe/core/report/transaction_log_report/transaction_log_report.py index 0a74ece322..e9c68cb0c7 100644 --- a/frappe/core/report/transaction_log_report/transaction_log_report.py +++ b/frappe/core/report/transaction_log_report/transaction_log_report.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019, Frappe Technologies and contributors +# Copyright (c) 2021, Frappe Technologies and contributors # License: MIT. See LICENSE import frappe @@ -12,13 +12,17 @@ def execute(filters=None): return columns, data def get_data(filters=None): - - logs = frappe.db.sql("SELECT * FROM `tabTransaction Log` order by creation desc ", as_dict=1) result = [] + logs = frappe.get_all("Transaction Log", fields=["*"], order_by="creation desc") + for l in logs: row_index = int(l.row_index) if row_index > 1: - previous_hash = frappe.db.sql("SELECT chaining_hash FROM `tabTransaction Log` WHERE row_index = {0}".format(row_index - 1)) + previous_hash = frappe.get_all( + "Transaction Log", + fields=["chaining_hash"], + filters={"row_index": row_index - 1}, + ) if not previous_hash: integrity = False else: diff --git a/frappe/database/database.py b/frappe/database/database.py index df5ad6dfda..c0d377fd42 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -170,6 +170,12 @@ class Database(object): frappe.errprint('Syntax error in query:') frappe.errprint(query) + elif self.is_deadlocked(e): + raise frappe.QueryDeadlockError + + elif self.is_timedout(e): + raise frappe.QueryTimeoutError + if ignore_ddl and (self.is_missing_column(e) or self.is_missing_table(e) or self.cant_drop_field_or_key(e)): pass else: 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/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py index aff1bd6973..b894a6bfbc 100644 --- a/frappe/desk/doctype/tag/tag.py +++ b/frappe/desk/doctype/tag/tag.py @@ -4,6 +4,7 @@ import frappe from frappe.model.document import Document from frappe.utils import unique +from frappe.query_builder import DocType class Tag(Document): pass @@ -42,10 +43,12 @@ def remove_tag(tag, dt, dn): @frappe.whitelist() def get_tagged_docs(doctype, tag): frappe.has_permission(doctype, throw=True) - - return frappe.db.sql("""SELECT name - FROM `tab{0}` - WHERE _user_tags LIKE '%{1}%'""".format(doctype, tag)) + doctype = DocType(doctype) + return ( + frappe.qb.from_(doctype) + .where(doctype._user_tags.like(tag)) + .select(doctype.name) + ).run() @frappe.whitelist() def get_tags(doctype, txt): 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 a755ec5e74..7fab90bee3 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -340,7 +340,7 @@ class EmailServer: return error_msg - def update_flag(self, uid_list={}): + def update_flag(self, uid_list=None): """ set all uids mails the flag as seen """ if not uid_list: diff --git a/frappe/exceptions.py b/frappe/exceptions.py index 4b59f8f38f..8449425bc1 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -101,6 +101,8 @@ class DataTooLongException(ValidationError): pass class FileAlreadyAttachedException(Exception): pass class DocumentAlreadyRestored(ValidationError): pass class AttachmentLimitReached(ValidationError): pass +class QueryTimeoutError(Exception): pass +class QueryDeadlockError(Exception): pass # OAuth exceptions class InvalidAuthorizationHeader(CSRFTokenError): pass class InvalidAuthorizationPrefix(CSRFTokenError): pass 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/model/naming.py b/frappe/model/naming.py index 71ff281642..deea6698b3 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -17,6 +17,7 @@ from frappe import _ from frappe.utils import now_datetime, cint, cstr import re from frappe.model import log_types +from frappe.query_builder import DocType def set_new_name(doc): @@ -194,7 +195,15 @@ def parse_naming_series(parts, doctype='', doc=''): def getseries(key, digits): # series created ? - current = frappe.db.sql("SELECT `current` FROM `tabSeries` WHERE `name`=%s FOR UPDATE", (key,)) + # Using frappe.qb as frappe.get_values does not allow order_by=None + series = DocType("Series") + current = ( + frappe.qb.from_(series) + .where(series.name == key) + .for_update() + .select("current") + ).run() + if current and current[0][0] is not None: current = current[0][0] # yes, update it @@ -260,7 +269,13 @@ def revert_series_if_last(key, name, doc=None): prefix = parse_naming_series(prefix.split('.'), doc=doc) count = cint(name.replace(prefix, "")) - current = frappe.db.sql("SELECT `current` FROM `tabSeries` WHERE `name`=%s FOR UPDATE", (prefix,)) + series = DocType("Series") + current = ( + frappe.qb.from_(series) + .where(series.name == prefix) + .for_update() + .select("current") + ).run() if current and current[0][0]==count: frappe.db.sql("UPDATE `tabSeries` SET `current` = `current` - 1 WHERE `name`=%s", prefix) 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 85df031073..c1b654d0e8 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/data.js b/frappe/public/js/frappe/form/controls/data.js index 864a0562ef..803c2cf070 100644 --- a/frappe/public/js/frappe/form/controls/data.js +++ b/frappe/public/js/frappe/form/controls/data.js @@ -15,11 +15,6 @@ frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlInp .addClass("input-with-feedback form-control") .prependTo(this.input_area); - if (in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'], - this.df.fieldtype)) { - this.$input.attr("maxlength", this.df.length || 140); - } - this.$input.on('paste', (e) => { let pasted_data = frappe.utils.get_clipboard_data(e); let maxlength = this.$input.attr('maxlength'); @@ -199,6 +194,13 @@ frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlInp } } set_input_attributes() { + if (in_list( + ['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only'], + this.df.fieldtype + )) { + this.$input.attr("maxlength", this.df.length || 140); + } + this.$input .attr("data-fieldtype", this.df.fieldtype) .attr("data-fieldname", this.df.fieldname) 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/form/footer/footer.js b/frappe/public/js/frappe/form/footer/footer.js index d6dfc227f0..3c89a86fee 100644 --- a/frappe/public/js/frappe/form/footer/footer.js +++ b/frappe/public/js/frappe/form/footer/footer.js @@ -30,7 +30,7 @@ frappe.ui.form.Footer = class FormFooter { fieldname: 'comment' }, on_submit: (comment) => { - if (strip_html(comment).trim() != "") { + if (strip_html(comment).trim() != "" || comment.includes('img')) { this.frm.comment_box.disable(); frappe.xcall("frappe.desk.form.utils.add_comment", { reference_doctype: this.frm.doctype, diff --git a/frappe/public/js/frappe/request.js b/frappe/public/js/frappe/request.js index 12fa9c8e21..adc3bb5626 100644 --- a/frappe/public/js/frappe/request.js +++ b/frappe/public/js/frappe/request.js @@ -206,6 +206,25 @@ frappe.request.call = function(opts) { } }; + var exception_handlers = { + 'QueryTimeoutError': function() { + frappe.utils.play_sound("error"); + frappe.msgprint({ + title: __('Request Timeout'), + indicator: 'red', + message: __("Server was too busy to process this request. Please try again.") + }); + }, + 'QueryDeadlockError': function() { + frappe.utils.play_sound("error"); + frappe.msgprint({ + title: __('Deadlock Occurred'), + indicator: 'red', + message: __("Server was too busy to process this request. Please try again.") + }); + } + }; + var ajax_args = { url: opts.url || frappe.request.url, data: opts.args, @@ -272,13 +291,25 @@ frappe.request.call = function(opts) { }) .fail(function(xhr, textStatus) { try { + if (xhr.responseText) { + var data = JSON.parse(xhr.responseText); + if (data.exception) { + // frappe.exceptions.CustomError -> CustomError + var exception = data.exception.split('.').at(-1); + var exception_handler = exception_handlers[exception]; + if (exception_handler) { + exception_handler(data); + return; + } + } + } var status_code_handler = statusCode[xhr.statusCode().status]; if (status_code_handler) { status_code_handler(xhr); - } else { - // if not handled by error handler! - opts.error_callback && opts.error_callback(xhr); + return; } + // if not handled by error handler! + opts.error_callback && opts.error_callback(xhr); } catch(e) { console.log("Unable to handle failed response"); // eslint-disable-line console.trace(e); // eslint-disable-line 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/login.bundle.scss b/frappe/public/scss/login.bundle.scss index 17f33b0a67..3963fbecc6 100644 --- a/frappe/public/scss/login.bundle.scss +++ b/frappe/public/scss/login.bundle.scss @@ -135,7 +135,7 @@ body { } .social-logins { - margin: var(--margin-md) 0; + margin-top: var(--margin-md); font-size: var(--text-md); .social-login-buttons { @@ -147,7 +147,11 @@ body { } min-width: 50%; padding: 0 4px; - margin-bottom: var(--margin-sm); + margin-bottom: var(--margin-md); + + &:last-child { + margin-bottom: 0; + } } } } diff --git a/frappe/public/scss/website.bundle.scss b/frappe/public/scss/website.bundle.scss index 06ec73c386..bcbb6f3c6a 100644 --- a/frappe/public/scss/website.bundle.scss +++ b/frappe/public/scss/website.bundle.scss @@ -1 +1 @@ -@import './website/index'; +@import './website/index'; \ No newline at end of file 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/public/scss/website/page_builder.scss b/frappe/public/scss/website/page_builder.scss index 252ad1bf9f..e7e2f8b242 100644 --- a/frappe/public/scss/website/page_builder.scss +++ b/frappe/public/scss/website/page_builder.scss @@ -145,11 +145,6 @@ .section-with-cards .card { @include transition(); - border: none; - - .card-body { - padding: 0 1.5rem 2rem 0; - } &:hover { border-color: $gray-500; 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"> - + + + + +