diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index cb502f68a7..3eefd1ce82 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -137,7 +137,7 @@ jobs: - name: UI Tests if: ${{ steps.check-build.outputs.build == 'strawberry' }} - run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --with-coverage --headless --parallel --ci-build-id $GITHUB_RUN_ID + run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --with-coverage --headless --parallel --ci-build-id $GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT env: CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb diff --git a/cypress/integration/control_barcode.js b/cypress/integration/control_barcode.js index 5f1ab86d41..03ab61fac4 100644 --- a/cypress/integration/control_barcode.js +++ b/cypress/integration/control_barcode.js @@ -21,7 +21,6 @@ context('Control Barcode', () => { get_dialog_with_barcode().as('dialog'); cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox') - .focus() .type('123456789') .blur(); cy.get('.frappe-control[data-fieldname=barcode] svg[data-barcode-value="123456789"]') @@ -38,7 +37,6 @@ context('Control Barcode', () => { get_dialog_with_barcode().as('dialog'); cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox') - .focus() .type('123456789') .blur(); cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox') diff --git a/cypress/integration/control_icon.js b/cypress/integration/control_icon.js index 5c531a0823..d89eba8840 100644 --- a/cypress/integration/control_icon.js +++ b/cypress/integration/control_icon.js @@ -19,18 +19,18 @@ context('Control Icon', () => { get_dialog_with_icon().as('dialog'); cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').click(); - cy.get('.icon-picker .icon-wrapper[id=active]').first().click(); - cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').should('have.value', 'active'); + cy.get('.icon-picker .icon-wrapper[id=heart-active]').first().click(); + cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').should('have.value', 'heart-active'); cy.get('@dialog').then(dialog => { let value = dialog.get_value('icon'); - expect(value).to.equal('active'); + expect(value).to.equal('heart-active'); }); - cy.get('.icon-picker .icon-wrapper[id=resting]').first().click(); - cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').should('have.value', 'resting'); + cy.get('.icon-picker .icon-wrapper[id=heart]').first().click(); + cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').should('have.value', 'heart'); cy.get('@dialog').then(dialog => { let value = dialog.get_value('icon'); - expect(value).to.equal('resting'); + expect(value).to.equal('heart'); }); }); diff --git a/cypress/integration/datetime.js b/cypress/integration/datetime.js index ef1952dc94..4a24faf40b 100644 --- a/cypress/integration/datetime.js +++ b/cypress/integration/datetime.js @@ -103,6 +103,7 @@ context('Control Date, Time and DateTime', () => { input_value: '12-02-2019 11:00' // admin timezone (Asia/Kolkata) } ]; + datetime_formats.forEach(d => { it(`test datetime format ${d.date_format} ${d.time_format}`, () => { cy.set_value('System Settings', 'System Settings', { diff --git a/cypress/integration/multi_select_dialog.js b/cypress/integration/multi_select_dialog.js index 7752ad0f0b..607db506c7 100644 --- a/cypress/integration/multi_select_dialog.js +++ b/cypress/integration/multi_select_dialog.js @@ -77,11 +77,11 @@ context('MultiSelectDialog', () => { it('tests more button', () => { cy.get_open_dialog() - .get(`.frappe-control[data-fieldname="more_btn"]`) + .get(`.frappe-control[data-fieldname="more_child_btn"]`) .should('exist') .as('more-btn'); - cy.get_open_dialog().get('.list-item-container').should(($rows) => { + cy.get_open_dialog().get('.datatable .dt-scrollable .dt-row').should(($rows) => { expect($rows).to.have.length(20); }); @@ -89,7 +89,7 @@ context('MultiSelectDialog', () => { cy.get('@more-btn').find('button').click({force: true}); cy.wait('@get-more-records'); - cy.get_open_dialog().get('.list-item-container').should(($rows) => { + cy.get_open_dialog().get('.datatable .dt-scrollable .dt-row').should(($rows) => { if ($rows.length <= 20) { throw new Error("More button doesn't work"); } diff --git a/cypress/integration/report_view.js b/cypress/integration/report_view.js index 629ae72eb8..0253e8fd43 100644 --- a/cypress/integration/report_view.js +++ b/cypress/integration/report_view.js @@ -7,8 +7,6 @@ context('Report View', () => { cy.visit('/app/website'); cy.insert_doc('DocType', custom_submittable_doctype, true); cy.clear_cache(); - }); - it('Field with enabled allow_on_submit should be editable.', () => { cy.insert_doc(doctype_name, { 'title': 'Doc 1', 'description': 'Random Text', @@ -16,6 +14,8 @@ context('Report View', () => { // submit document 'docstatus': 1 }, true).as('doc'); + }); + it('Field with enabled allow_on_submit should be editable.', () => { cy.intercept('POST', 'api/method/frappe.client.set_value').as('value-update'); cy.visit(`/app/List/${doctype_name}/Report`); // check status column added from docstatus diff --git a/cypress/integration/timeline_email.js b/cypress/integration/timeline_email.js index dfe80e0019..5808bd52ef 100644 --- a/cypress/integration/timeline_email.js +++ b/cypress/integration/timeline_email.js @@ -14,12 +14,12 @@ context('Timeline Email', () => { cy.wait(700); }); - it('Adding email and verifying timeline content for email attachment, deleting attachment and ToDo', () => { + it('Adding email and verifying timeline content for email attachment', () => { cy.visit('/app/todo'); cy.get('.list-row > .level-left > .list-subject').eq(0).click(); //Creating a new email - cy.get('.timeline-actions > .btn').click(); + cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click(); cy.fill_field('recipients', 'test@example.com', 'MultiSelect'); cy.get('.modal.show > .modal-dialog > .modal-content > .modal-body > :nth-child(1) > .form-layout > .form-page > :nth-child(3) > .section-body > .form-column > form > [data-fieldtype="Text Editor"] > .form-group > .control-input-wrapper > .control-input > .ql-container > .ql-editor').type('Test Mail'); @@ -43,7 +43,9 @@ context('Timeline Email', () => { cy.get('#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .btn').click(); cy.get('#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .dropdown-menu > li > .grey-link').eq(9).click(); cy.get('.modal.show > .modal-dialog > .modal-content > .modal-footer > .standard-actions > .btn-primary').click(); + }); + it('Deleting attachment and ToDo', () => { cy.visit('/app/todo'); cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click(); @@ -57,11 +59,11 @@ context('Timeline Email', () => { cy.wait(500); //To check if the discard button functionality in email is working correctly - cy.get('.timeline-actions > .btn').click(); + cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click(); cy.fill_field('recipients', 'test@example.com', 'MultiSelect'); cy.get('.modal-footer > .standard-actions > .btn-secondary').contains('Discard').click(); cy.wait(500); - cy.get('.timeline-actions > .btn').click(); + cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click(); cy.wait(500); cy.get_field('recipients', 'MultiSelect').should('have.text', ''); cy.get('.modal-header:visible > .modal-actions > .btn-modal-close > .icon').click(); diff --git a/cypress/integration/workspace.js b/cypress/integration/workspace.js index 65586366e6..9d6eeaff64 100644 --- a/cypress/integration/workspace.js +++ b/cypress/integration/workspace.js @@ -33,44 +33,39 @@ context('Workspace 2.0', () => { }); it('Add New Block', () => { - cy.get('.codex-editor__redactor .ce-block'); - cy.get('.custom-actions .inner-group-button[data-label="Add%20Block"]').click(); - cy.get('.custom-actions .inner-group-button .dropdown-menu .block-menu-item-label').contains('Heading').click(); + cy.get('.ce-block').click().type('{enter}'); + cy.get('.block-list-container .block-list-item').contains('Heading').click(); cy.get(":focus").type('Header'); cy.get(".ce-block:last").find('.ce-header').should('exist'); - cy.get('.custom-actions .inner-group-button[data-label="Add%20Block"]').click(); - cy.get('.custom-actions .inner-group-button .dropdown-menu .block-menu-item-label').contains('Text').click(); + cy.get('.ce-block:last').click().type('{enter}'); + cy.get('.block-list-container .block-list-item').contains('Text').click(); cy.get(":focus").type('Paragraph text'); cy.get(".ce-block:last").find('.ce-paragraph').should('exist'); }); it('Delete A Block', () => { - cy.get(".ce-block:last").find('.delete-paragraph').click(); + cy.get(":focus").click(); + cy.get('.paragraph-control .setting-btn').click(); + cy.get('.paragraph-control .dropdown-item').contains('Delete').click(); cy.get(".ce-block:last").find('.ce-paragraph').should('not.exist'); }); it('Shrink and Expand A Block', () => { - cy.get(".ce-block:last").find('.tune-btn').click(); - cy.get('.ce-settings--opened .ce-shrink-button').click(); - cy.get(".ce-block:last").should('have.class', 'col-11'); - cy.get('.ce-settings--opened .ce-shrink-button').click(); - cy.get(".ce-block:last").should('have.class', 'col-10'); - cy.get('.ce-settings--opened .ce-shrink-button').click(); - cy.get(".ce-block:last").should('have.class', 'col-9'); - cy.get('.ce-settings--opened .ce-expand-button').click(); - cy.get(".ce-block:last").should('have.class', 'col-10'); - cy.get('.ce-settings--opened .ce-expand-button').click(); - cy.get(".ce-block:last").should('have.class', 'col-11'); - cy.get('.ce-settings--opened .ce-expand-button').click(); - cy.get(".ce-block:last").should('have.class', 'col-12'); - }); - - it('Change Header Text Size', () => { - cy.get('.ce-settings--opened .cdx-settings-button[data-level="3"]').click(); - cy.get(".ce-block:last").find('.widget-head h3').should('exist'); - cy.get('.ce-settings--opened .cdx-settings-button[data-level="4"]').click(); - cy.get(".ce-block:last").find('.widget-head h4').should('exist'); + cy.get(":focus").click(); + cy.get('.ce-block:last .setting-btn').click(); + cy.get('.ce-block:last .dropdown-item').contains('Shrink').click(); + cy.get(".ce-block:last").should('have.class', 'col-xs-11'); + cy.get('.ce-block:last .dropdown-item').contains('Shrink').click(); + cy.get(".ce-block:last").should('have.class', 'col-xs-10'); + cy.get('.ce-block:last .dropdown-item').contains('Shrink').click(); + cy.get(".ce-block:last").should('have.class', 'col-xs-9'); + cy.get('.ce-block:last .dropdown-item').contains('Expand').click(); + cy.get(".ce-block:last").should('have.class', 'col-xs-10'); + cy.get('.ce-block:last .dropdown-item').contains('Expand').click(); + cy.get(".ce-block:last").should('have.class', 'col-xs-11'); + cy.get('.ce-block:last .dropdown-item').contains('Expand').click(); + cy.get(".ce-block:last").should('have.class', 'col-xs-12'); cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click(); }); @@ -79,7 +74,10 @@ context('Workspace 2.0', () => { cy.get('.codex-editor__redactor .ce-block'); cy.get('.standard-actions .btn-secondary[data-label=Edit]').click(); - cy.get('.sidebar-item-container[item-name="Test Private Page"]').find('.sidebar-item-control .delete-page').click(); + cy.get('.sidebar-item-container[item-name="Test Private Page"]') + .find('.sidebar-item-control .setting-btn').click(); + cy.get('.sidebar-item-container[item-name="Test Private Page"]') + .find('.dropdown-item[title="Delete Workspace"]').click({force: true}); cy.wait(300); cy.get('.modal-footer > .standard-actions > .btn-modal-primary:visible').first().click(); cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click(); diff --git a/frappe/__init__.py b/frappe/__init__.py index a8bf114b9b..c6cbfead43 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -102,7 +102,7 @@ def as_unicode(text, encoding='utf-8'): '''Convert to unicode if required''' if isinstance(text, str): return text - elif text==None: + elif text is None: return '' elif isinstance(text, bytes): return str(text, encoding) @@ -294,7 +294,7 @@ def get_conf(site=None): class init_site: def __init__(self, site=None): - '''If site==None, initialize it for empty site ('') to load common_site_config.json''' + '''If site is None, initialize it for empty site ('') to load common_site_config.json''' self.site = site or '' def __enter__(self): @@ -446,7 +446,7 @@ def throw(msg, exc=ValidationError, title=None, is_minimizable=None, wide=None, msgprint(msg, raise_exception=exc, title=title, indicator='red', is_minimizable=is_minimizable, wide=wide, as_list=as_list) def emit_js(js, user=False, **kwargs): - if user == False: + if user is False: user = session.user publish_realtime('eval_js', js, user=user, **kwargs) @@ -1661,7 +1661,7 @@ def local_cache(namespace, key, generator, regenerate_if_none=False): if key not in local.cache[namespace]: local.cache[namespace][key] = generator() - elif local.cache[namespace][key]==None and regenerate_if_none: + elif local.cache[namespace][key] is None and regenerate_if_none: # if key exists but the previous result was None local.cache[namespace][key] = generator() diff --git a/frappe/auth.py b/frappe/auth.py index a87edb6460..d4778eb0c1 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -111,7 +111,8 @@ class LoginManager: self.user_type = None if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login": - if self.login()==False: return + if self.login() is False: + return self.resume = False # run login triggers diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.py b/frappe/automation/doctype/assignment_rule/assignment_rule.py index a8c75bffd9..90099eebb6 100644 --- a/frappe/automation/doctype/assignment_rule/assignment_rule.py +++ b/frappe/automation/doctype/assignment_rule/assignment_rule.py @@ -272,7 +272,7 @@ def apply(doc=None, method=None, doctype=None, name=None): for todo in todos_to_close: _todo = frappe.get_doc("ToDo", todo) _todo.status = "Closed" - _todo.save() + _todo.save(ignore_permissions=True) break else: diff --git a/frappe/automation/workspace/tools/tools.json b/frappe/automation/workspace/tools/tools.json index fa2606dc43..40b265b34f 100644 --- a/frappe/automation/workspace/tools/tools.json +++ b/frappe/automation/workspace/tools/tools.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"ToDo\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Note\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"File\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Assignment Rule\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Auto Repeat\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tools\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Email\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Automation\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Event Streaming\", \"col\": 4}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"ToDo\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Note\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"File\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Assignment Rule\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Auto Repeat\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Email\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Automation\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Event Streaming\",\"col\":4}}]", "creation": "2020-03-02 14:53:24.980279", "docstatus": 0, "doctype": "Workspace", @@ -208,7 +208,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:16:02.839181", + "modified": "2022-01-13 17:48:48.456763", "modified_by": "Administrator", "module": "Automation", "name": "Tools", @@ -217,7 +217,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 26, + "sequence_id": 26.0, "shortcuts": [ { "label": "ToDo", diff --git a/frappe/boot.py b/frappe/boot.py index d5d992343a..f1fd0f6a3b 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -107,8 +107,8 @@ def load_conf_settings(bootinfo): if key in conf: bootinfo[key] = conf.get(key) def load_desktop_data(bootinfo): - from frappe.desk.desktop import get_wspace_sidebar_items - bootinfo.allowed_workspaces = get_wspace_sidebar_items().get('pages') + from frappe.desk.desktop import get_workspace_sidebar_items + bootinfo.allowed_workspaces = get_workspace_sidebar_items().get('pages') bootinfo.module_page_map = get_controller("Workspace").get_module_page_map() bootinfo.dashboards = frappe.get_all("Dashboard") diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index 0df8878da4..94a845639b 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -148,7 +148,7 @@ def build_table_count_cache(): data = ( frappe.qb.from_(information_schema.tables).select(table_name, table_rows) ).run(as_dict=True) - counts = {d.get('name').lstrip('tab'): d.get('count', None) for d in data} + counts = {d.get('name').replace('tab', '', 1): d.get('count', None) for d in data} _cache.set_value("information_schema:counts", counts) return counts diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 677325e02d..62488525b0 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -952,7 +952,7 @@ def trim_database(context, dry_run, format, no_backup): doctype_tables = frappe.get_all("DocType", pluck="name") for x in database_tables: - doctype = x.lstrip("tab") + doctype = x.replace("tab", "", 1) if not (doctype in doctype_tables or x.startswith("__") or x in STANDARD_TABLES): TABLES_TO_DROP.append(x) @@ -966,7 +966,7 @@ def trim_database(context, dry_run, format, no_backup): odb = scheduled_backup( ignore_conf=False, - include_doctypes=",".join(x.lstrip("tab") for x in TABLES_TO_DROP), + include_doctypes=",".join(x.replace("tab", "", 1) for x in TABLES_TO_DROP), ignore_files=True, force=True, ) diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.py b/frappe/core/doctype/document_naming_rule/document_naming_rule.py index 8013f9df6f..5c445fd058 100644 --- a/frappe/core/doctype/document_naming_rule/document_naming_rule.py +++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.py @@ -5,6 +5,7 @@ import frappe from frappe.model.document import Document from frappe.utils.data import evaluate_filters +from frappe.model.naming import parse_naming_series from frappe import _ class DocumentNamingRule(Document): @@ -27,7 +28,9 @@ class DocumentNamingRule(Document): return counter = frappe.db.get_value(self.doctype, self.name, 'counter', for_update=True) or 0 - doc.name = self.prefix + ('%0'+str(self.prefix_digits)+'d') % (counter + 1) + naming_series = parse_naming_series(self.prefix, doc=doc) + + doc.name = naming_series + ('%0'+str(self.prefix_digits)+'d') % (counter + 1) frappe.db.set_value(self.doctype, self.name, 'counter', counter + 1) @frappe.whitelist() diff --git a/frappe/core/doctype/payment_gateway/payment_gateway.json b/frappe/core/doctype/payment_gateway/payment_gateway.json index b97d72c771..7195b3949e 100644 --- a/frappe/core/doctype/payment_gateway/payment_gateway.json +++ b/frappe/core/doctype/payment_gateway/payment_gateway.json @@ -1,154 +1,55 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:gateway", - "beta": 0, - "creation": "2015-12-15 22:26:45.221162", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, + "actions": [], + "autoname": "field:gateway", + "creation": "2022-01-24 21:09:47.229371", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "gateway", + "gateway_settings", + "gateway_controller" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "gateway", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Gateway", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "gateway", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Gateway", + "reqd": 1, + "unique": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "gateway_settings", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Gateway Settings", - "length": 0, - "no_copy": 0, - "options": "DocType", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "gateway_settings", + "fieldtype": "Link", + "label": "Gateway Settings", + "options": "DocType" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "gateway_controller", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Gateway Controller", - "length": 0, - "no_copy": 0, - "options": "gateway_settings", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "gateway_controller", + "fieldtype": "Dynamic Link", + "label": "Gateway Controller", + "options": "gateway_settings" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 1, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-02-05 14:24:33.526645", - "modified_by": "Administrator", - "module": "Core", - "name": "Payment Gateway", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "modified": "2022-01-24 21:17:03.864719", + "modified_by": "Administrator", + "module": "Core", + "name": "Payment Gateway", + "naming_rule": "By fieldname", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "create": 1, + "delete": 1, + "read": 1, + "role": "System Manager", + "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index d8c945fb6d..b5f3ba7168 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -34,19 +34,7 @@ def run_server_script_for_doc_event(doc, event): if scripts: # run all scripts for this doctype + event for script_name in scripts: - try: - frappe.get_doc('Server Script', script_name).execute_doc(doc) - except Exception as e: - message = frappe._('Error executing Server Script {0}. Open Browser Console to see traceback.').format( - frappe.utils.get_link_to_form('Server Script', script_name) - ) - exception = type(e) - if getattr(frappe, 'request', None): - # all exceptions throw 500 which is internal server error - # however server script error is a user error - # so we should throw 417 which is expectation failed - exception.http_status_code = 417 - frappe.throw(title=frappe._('Server Script Error'), msg=message, exc=exception) + frappe.get_doc('Server Script', script_name).execute_doc(doc) def get_server_script_map(): # fetch cached server script methods diff --git a/frappe/core/doctype/test/test.py b/frappe/core/doctype/test/test.py index 4cb088c117..ab6fcb6de4 100644 --- a/frappe/core/doctype/test/test.py +++ b/frappe/core/doctype/test/test.py @@ -31,4 +31,15 @@ class test(Document): def get_value(self, fields, filters, **kwargs): # return [] with open("data_file.json", "r") as read_file: - return [json.load(read_file)] \ No newline at end of file + return [json.load(read_file)] + + def get_count(self, args): + # return [] + with open("data_file.json", "r") as read_file: + return [json.load(read_file)] + + def get_stats(self, args): + # return [] + with open("data_file.json", "r") as read_file: + return [json.load(read_file)] + diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index b674ea6891..f1ccc25c6e 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -344,7 +344,7 @@ class User(Document): frappe.sendmail(recipients=self.email, sender=sender, subject=subject, template=template, args=args, header=[subject, "green"], - delayed=(not now) if now!=None else self.flags.delay_emails, retry=3) + delayed=(not now) if now is not None else self.flags.delay_emails, retry=3) def a_system_manager_should_exist(self): if not self.get_other_system_managers(): diff --git a/frappe/core/doctype/user_type/user_type.py b/frappe/core/doctype/user_type/user_type.py index 661ac932e7..626ab772b8 100644 --- a/frappe/core/doctype/user_type/user_type.py +++ b/frappe/core/doctype/user_type/user_type.py @@ -121,7 +121,7 @@ class UserType(Document): for child_table in doc.get_table_fields(): child_doc = frappe.get_meta(child_table.options) - if not child_doc.istable: + if child_doc: self.prepare_select_perm_doctypes(child_doc, user_doctypes, select_doctypes) if select_doctypes: diff --git a/frappe/core/workspace/build/build.json b/frappe/core/workspace/build/build.json index aabb4f9d1c..c1c506ae3a 100644 --- a/frappe/core/workspace/build/build.json +++ b/frappe/core/workspace/build/build.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"DocType\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Workspace\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Report\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Elements\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Modules\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Models\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Views\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Scripting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Packages\",\"col\":4}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"DocType\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Workspace\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Report\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Elements\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Modules\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Models\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Views\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Scripting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Packages\",\"col\":4}}]", "creation": "2021-01-02 10:51:16.579957", "docstatus": 0, "doctype": "Workspace", @@ -222,7 +222,7 @@ "type": "Link" } ], - "modified": "2021-09-05 21:14:52.384816", + "modified": "2022-01-13 17:26:02.736366", "modified_by": "Administrator", "module": "Core", "name": "Build", @@ -231,7 +231,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 5, + "sequence_id": 5.0, "shortcuts": [ { "doc_view": "", diff --git a/frappe/core/workspace/settings/settings.json b/frappe/core/workspace/settings/settings.json index 917ce2cbdc..5aadbc42d5 100644 --- a/frappe/core/workspace/settings/settings.json +++ b/frappe/core/workspace/settings/settings.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\":\"header\",\"data\": {\"text\":\"Settings\",\"level\": 4,\"col\": 12}}, {\"type\":\"shortcut\",\"data\": {\"shortcut_name\":\"System Settings\",\"col\": 4}}, {\"type\":\"shortcut\",\"data\": {\"shortcut_name\":\"Print Settings\",\"col\": 4}}, {\"type\":\"shortcut\",\"data\": {\"shortcut_name\":\"Website Settings\",\"col\": 4}}, {\"type\":\"spacer\",\"data\": {\"col\": 12}}, {\"type\":\"header\",\"data\": {\"text\":\"Reports & Masters\",\"level\": 4,\"col\": 12}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Data\",\"col\": 4}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Email / Notifications\",\"col\": 4}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Website\",\"col\": 4}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Core\",\"col\": 4}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Printing\",\"col\": 4}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Workflow\",\"col\": 4}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Settings\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"System Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Website Settings\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Email / Notifications\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Website\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Core\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Printing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Workflow\",\"col\":4}}]", "creation": "2020-03-02 15:09:40.527211", "docstatus": 0, "doctype": "Workspace", @@ -367,7 +367,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:16:03.456174", + "modified": "2022-01-13 17:49:59.586909", "modified_by": "Administrator", "module": "Core", "name": "Settings", @@ -376,7 +376,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 29, + "sequence_id": 29.0, "shortcuts": [ { "icon": "setting", diff --git a/frappe/core/workspace/users/users.json b/frappe/core/workspace/users/users.json index 85c110151b..5741c54eeb 100644 --- a/frappe/core/workspace/users/users.json +++ b/frappe/core/workspace/users/users.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"User\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Role\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Permission Manager\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"User Profile\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"User Type\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Users\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Logs\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Permissions\", \"col\": 4}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Role\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Permission Manager\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User Profile\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User Type\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Users\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Logs\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Permissions\",\"col\":4}}]", "creation": "2020-03-02 15:12:16.754449", "docstatus": 0, "doctype": "Workspace", @@ -145,7 +145,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:16:03.010205", + "modified": "2022-01-13 17:49:08.912772", "modified_by": "Administrator", "module": "Core", "name": "Users", @@ -154,7 +154,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 27, + "sequence_id": 27.0, "shortcuts": [ { "label": "User", diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 24a5d1358b..1593ed49a5 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -107,20 +107,26 @@ class CustomizeForm(Document): def set_name_translation(self): '''Create, update custom translation for this doctype''' current = self.get_name_translation() - if current: - if self.label and current.translated_text != self.label: - frappe.db.set_value('Translation', current.name, 'translated_text', self.label) - frappe.translate.clear_cache() - else: + if not self.label: + if current: # clear translation frappe.delete_doc('Translation', current.name) + return - else: - if self.label: - frappe.get_doc(dict(doctype='Translation', - source_text=self.doc_type, - translated_text=self.label, - language_code=frappe.local.lang or 'en')).insert() + if not current: + frappe.get_doc( + { + "doctype": 'Translation', + "source_text": self.doc_type, + "translated_text": self.label, + "language_code": frappe.local.lang or 'en' + } + ).insert() + return + + if self.label != current.translated_text: + frappe.db.set_value('Translation', current.name, 'translated_text', self.label) + frappe.translate.clear_cache() def clear_existing_doc(self): doc_type = self.doc_type @@ -377,7 +383,7 @@ class CustomizeForm(Document): def make_property_setter(self, prop, value, property_type, fieldname=None, apply_on=None, row_name = None): - delete_property_setter(self.doc_type, prop, fieldname) + delete_property_setter(self.doc_type, prop, fieldname, row_name) property_value = self.get_existing_property_value(prop, fieldname) diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py index 8a287b17e8..0fe39e0008 100644 --- a/frappe/custom/doctype/customize_form/test_customize_form.py +++ b/frappe/custom/doctype/customize_form/test_customize_form.py @@ -304,3 +304,25 @@ class TestCustomizeForm(unittest.TestCase): action = [d for d in event.actions if d.label=='Test Action'] self.assertEqual(len(action), 0) + + def test_custom_label(self): + d = self.get_customize_form("Event") + + # add label + d.label = "Test Rename" + d.run_method("save_customization") + self.assertEqual(d.label, "Test Rename") + + # change label + d.label = "Test Rename 2" + d.run_method("save_customization") + self.assertEqual(d.label, "Test Rename 2") + + # saving again to make sure existing label persists + d.run_method("save_customization") + self.assertEqual(d.label, "Test Rename 2") + + # clear label + d.label = "" + d.run_method("save_customization") + self.assertEqual(d.label, "") diff --git a/frappe/custom/doctype/property_setter/property_setter.py b/frappe/custom/doctype/property_setter/property_setter.py index 7f40be9725..0a65aa6f5d 100644 --- a/frappe/custom/doctype/property_setter/property_setter.py +++ b/frappe/custom/doctype/property_setter/property_setter.py @@ -19,7 +19,7 @@ class PropertySetter(Document): def validate(self): self.validate_fieldtype_change() if self.is_new(): - delete_property_setter(self.doc_type, self.property, self.field_name) + delete_property_setter(self.doc_type, self.property, self.field_name, self.row_name) # clear cache frappe.clear_cache(doctype = self.doc_type) @@ -91,11 +91,13 @@ def make_property_setter(doctype, fieldname, property, value, property_type, for property_setter.insert() return property_setter -def delete_property_setter(doc_type, property, field_name=None): +def delete_property_setter(doc_type, property, field_name=None, row_name=None): """delete other property setters on this, if this is new""" - filters = dict(doc_type = doc_type, property=property) + filters = dict(doc_type=doc_type, property=property) if field_name: filters['field_name'] = field_name + if row_name: + filters["row_name"] = row_name frappe.db.delete('Property Setter', filters) diff --git a/frappe/custom/workspace/customization/customization.json b/frappe/custom/workspace/customization/customization.json index 8938bdec9c..1756abcb1d 100644 --- a/frappe/custom/workspace/customization/customization.json +++ b/frappe/custom/workspace/customization/customization.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Customization\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customize Form\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Custom Role\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Client Script\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Server Script\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Dashboards\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Form Customization\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other\",\"col\":4}}]", + "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Customization\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customize Form\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Custom Role\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Client Script\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Server Script\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Dashboards\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Form Customization\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other\",\"col\":4}}]", "creation": "2020-03-02 15:15:03.839594", "docstatus": 0, "doctype": "Workspace", @@ -123,7 +123,7 @@ "type": "Link" } ], - "modified": "2021-11-24 16:20:03.500885", + "modified": "2022-01-13 17:28:08.345794", "modified_by": "Administrator", "module": "Custom", "name": "Customization", @@ -132,7 +132,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 8, + "sequence_id": 8.0, "shortcuts": [ { "label": "Customize Form", diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index 6b827a4e89..de28dad900 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -245,9 +245,16 @@ class MariaDBDatabase(Database): column_name as 'name', column_type as 'type', column_default as 'default', - column_key = 'MUL' as 'index', + COALESCE( + (select 1 + from information_schema.statistics + where table_name="{table_name}" + and column_name=columns.column_name + and NON_UNIQUE=1 + limit 1 + ), 0) as 'index', column_key = 'UNI' as 'unique' - from information_schema.columns + from information_schema.columns as columns where table_name = '{table_name}' '''.format(table_name=table_name), as_dict=1) def has_index(self, table_name, index_name): diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py index 5768a2f23d..07bb4d5d7c 100644 --- a/frappe/database/mariadb/schema.py +++ b/frappe/database/mariadb/schema.py @@ -58,18 +58,34 @@ class MariaDBTable(DBTable): modify_column_query.append("MODIFY `{}` {}".format(col.fieldname, col.get_definition())) for col in self.add_index: - # if index key not exists - if not frappe.db.sql("SHOW INDEX FROM `%s` WHERE key_name = %s" % - (self.table_name, '%s'), col.fieldname): - add_index_query.append("ADD INDEX `{}`(`{}`)".format(col.fieldname, col.fieldname)) + # if index key does not exists + if not frappe.db.has_index(self.table_name, col.fieldname + '_index'): + add_index_query.append("ADD INDEX `{}_index`(`{}`)".format(col.fieldname, col.fieldname)) - for col in self.drop_index: + for col in self.drop_index + self.drop_unique: if col.fieldname != 'name': # primary key + current_column = self.current_columns.get(col.fieldname.lower()) + unique_constraint_changed = current_column.unique != col.unique + if unique_constraint_changed and not col.unique: + # nosemgrep + unique_index_record = frappe.db.sql(""" + SHOW INDEX FROM `{0}` + WHERE Key_name=%s + AND Non_unique=0 + """.format(self.table_name), (col.fieldname), as_dict=1) + if unique_index_record: + drop_index_query.append("DROP INDEX `{}`".format(unique_index_record[0].Key_name)) + index_constraint_changed = current_column.index != col.set_index # if index key exists - if frappe.db.sql("""SHOW INDEX FROM `{0}` - WHERE key_name=%s - AND Non_unique=%s""".format(self.table_name), (col.fieldname, col.unique)): - drop_index_query.append("drop index `{}`".format(col.fieldname)) + if index_constraint_changed and not col.set_index: + # nosemgrep + index_record = frappe.db.sql(""" + SHOW INDEX FROM `{0}` + WHERE Key_name=%s + AND Non_unique=1 + """.format(self.table_name), (col.fieldname + '_index'), as_dict=1) + if index_record: + drop_index_query.append("DROP INDEX `{}`".format(index_record[0].Key_name)) try: for query_parts in [add_column_query, modify_column_query, add_index_query, drop_index_query]: diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index 33f07990af..d5495c6879 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -77,11 +77,11 @@ class PostgresDatabase(Database): """Escape quotes and percent in given string.""" if isinstance(s, bytes): s = s.decode('utf-8') - + # MariaDB's driver treats None as an empty string # So Postgres should do the same - if s is None: + if s is None: s = '' if percent: @@ -308,18 +308,20 @@ class PostgresDatabase(Database): WHEN 'timestamp without time zone' THEN 'timestamp' ELSE a.data_type END AS type, - COUNT(b.indexdef) AS Index, + BOOL_OR(b.index) AS index, SPLIT_PART(COALESCE(a.column_default, NULL), '::', 1) AS default, BOOL_OR(b.unique) AS unique FROM information_schema.columns a LEFT JOIN - (SELECT indexdef, tablename, indexdef LIKE '%UNIQUE INDEX%' AS unique + (SELECT indexdef, tablename, + indexdef LIKE '%UNIQUE INDEX%' AS unique, + indexdef NOT LIKE '%UNIQUE INDEX%' AS index FROM pg_indexes WHERE tablename='{table_name}') b - ON SUBSTRING(b.indexdef, '\(.*\)') LIKE CONCAT('%', a.column_name, '%') + ON SUBSTRING(b.indexdef, '(.*)') LIKE CONCAT('%', a.column_name, '%') WHERE a.table_name = '{table_name}' - GROUP BY a.column_name, a.data_type, a.column_default, a.character_maximum_length;''' - .format(table_name=table_name), as_dict=1) + GROUP BY a.column_name, a.data_type, a.column_default, a.character_maximum_length; + '''.format(table_name=table_name), as_dict=1) def get_database_list(self, target): return [d[0] for d in self.sql("SELECT datname FROM pg_database;")] diff --git a/frappe/database/postgres/schema.py b/frappe/database/postgres/schema.py index 58153ca6ce..a2d5be0b70 100644 --- a/frappe/database/postgres/schema.py +++ b/frappe/database/postgres/schema.py @@ -11,8 +11,6 @@ class PostgresTable(DBTable): column_defs = self.get_column_definitions() if column_defs: add_text += ',\n'.join(column_defs) - # index - # index_defs = self.get_index_definitions() # TODO: set docstatus length # create table frappe.db.sql("""create table `%s` ( @@ -28,8 +26,25 @@ class PostgresTable(DBTable): idx bigint not null default '0', %s)""".format(varchar_len=frappe.db.VARCHAR_LEN) % (self.table_name, add_text)) + self.create_indexes() frappe.db.commit() + def create_indexes(self): + create_index_query = "" + for key, col in self.columns.items(): + if (col.set_index + and col.fieldtype in frappe.db.type_map + and frappe.db.type_map.get(col.fieldtype)[0] + not in ('text', 'longtext')): + create_index_query += 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format( + index_name=col.fieldname, + table_name=self.table_name, + field=col.fieldname + ) + if create_index_query: + # nosemgrep + frappe.db.sql(create_index_query) + def alter(self): for col in self.columns.values(): col.build_for_alter_table(self.current_columns.get(col.fieldname.lower())) @@ -52,8 +67,8 @@ class PostgresTable(DBTable): query.append("ALTER COLUMN `{0}` TYPE {1} {2}".format( col.fieldname, get_definition(col.fieldtype, precision=col.precision, length=col.length), - using_clause) - ) + using_clause + )) for col in self.set_default: if col.fieldname=="name": @@ -73,37 +88,54 @@ class PostgresTable(DBTable): query.append("ALTER COLUMN `{}` SET DEFAULT {}".format(col.fieldname, col_default)) - create_index_query = "" + create_contraint_query = "" for col in self.add_index: # if index key not exists - create_index_query += 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format( + create_contraint_query += 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format( index_name=col.fieldname, table_name=self.table_name, field=col.fieldname) - drop_index_query = "" + for col in self.add_unique: + # if index key not exists + create_contraint_query += 'CREATE UNIQUE INDEX IF NOT EXISTS "unique_{index_name}" ON `{table_name}`(`{field}`);'.format( + index_name=col.fieldname, + table_name=self.table_name, + field=col.fieldname + ) + + drop_contraint_query = "" for col in self.drop_index: # primary key if col.fieldname != 'name': # if index key exists - if not frappe.db.has_index(self.table_name, col.fieldname): - drop_index_query += 'DROP INDEX IF EXISTS "{}" ;'.format(col.fieldname) + drop_contraint_query += 'DROP INDEX IF EXISTS "{}" ;'.format(col.fieldname) - if query: - try: + for col in self.drop_unique: + # primary key + if col.fieldname != 'name': + # if index key exists + drop_contraint_query += 'DROP INDEX IF EXISTS "unique_{}" ;'.format(col.fieldname) + try: + if query: final_alter_query = "ALTER TABLE `{}` {}".format(self.table_name, ", ".join(query)) - if final_alter_query: frappe.db.sql(final_alter_query) - if create_index_query: frappe.db.sql(create_index_query) - if drop_index_query: frappe.db.sql(drop_index_query) - except Exception as e: - # sanitize - if frappe.db.is_duplicate_fieldname(e): - frappe.throw(str(e)) - elif frappe.db.is_duplicate_entry(e): - fieldname = str(e).split("'")[-2] - frappe.throw(_("""{0} field cannot be set as unique in {1}, - as there are non-unique existing values""".format( - fieldname, self.table_name))) - raise e - else: - raise e + # nosemgrep + frappe.db.sql(final_alter_query) + if create_contraint_query: + # nosemgrep + frappe.db.sql(create_contraint_query) + if drop_contraint_query: + # nosemgrep + frappe.db.sql(drop_contraint_query) + except Exception as e: + # sanitize + if frappe.db.is_duplicate_fieldname(e): + frappe.throw(str(e)) + elif frappe.db.is_duplicate_entry(e): + fieldname = str(e).split("'")[-2] + frappe.throw( + _("{0} field cannot be set as unique in {1}, as there are non-unique existing values") + .format(fieldname, self.table_name) + ) + else: + raise e diff --git a/frappe/database/query.py b/frappe/database/query.py index 6d2be5fa25..587378b32f 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -308,7 +308,7 @@ class Permission: doctype = [doctype] for dt in doctype: - dt = re.sub("tab", "", dt) + dt = re.sub("^tab", "", dt) if not frappe.has_permission( dt, "select", diff --git a/frappe/database/schema.py b/frappe/database/schema.py index 10582eff8f..9a6dd502dc 100644 --- a/frappe/database/schema.py +++ b/frappe/database/schema.py @@ -21,6 +21,7 @@ class DBTable: self.change_name = [] self.add_unique = [] self.add_index = [] + self.drop_unique = [] self.drop_index = [] self.set_default = [] @@ -219,8 +220,10 @@ class DbColumn: self.table.change_type.append(self) # unique - if((self.unique and not current_def['unique']) and column_type not in ('text', 'longtext')): + if ((self.unique and not current_def['unique']) and column_type not in ('text', 'longtext')): self.table.add_unique.append(self) + elif (current_def['unique'] and not self.unique) and column_type not in ('text', 'longtext'): + self.table.drop_unique.append(self) # default if (self.default_changed(current_def) @@ -230,9 +233,7 @@ class DbColumn: self.table.set_default.append(self) # index should be applied or dropped irrespective of type change - if ((current_def['index'] and not self.set_index and not self.unique) - or (current_def['unique'] and not self.unique)): - # to drop unique you have to drop index + if (current_def['index'] and not self.set_index) and column_type not in ('text', 'longtext'): self.table.drop_index.append(self) elif (not current_def['index'] and self.set_index) and not (column_type in ('text', 'longtext')): diff --git a/frappe/defaults.py b/frappe/defaults.py index eb98db449f..e249ef2099 100644 --- a/frappe/defaults.py +++ b/frappe/defaults.py @@ -126,7 +126,7 @@ def set_default(key, value, parent, parenttype="__default"): "defkey": key, "parent": parent }) - if value != None: + if value is not None: add_default(key, value, parent) else: _clear_cache(parent) @@ -187,7 +187,7 @@ def get_defaults_for(parent="__default"): """get all defaults""" defaults = frappe.cache().hget("defaults", parent) - if defaults==None: + if defaults is None: # sort descending because first default must get precedence table = DocType("DefaultValue") res = frappe.qb.from_(table).where( diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index e1789852f1..4164db679d 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -56,31 +56,6 @@ class Workspace: self.restricted_doctypes = frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache() self.restricted_pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache() - def is_page_allowed(self): - cards = self.doc.get_link_groups() + get_custom_reports_and_doctypes(self.doc.module) - shortcuts = self.doc.shortcuts - - for section in cards: - links = loads(section.get('links')) if isinstance(section.get('links'), str) else section.get('links') - for item in links: - if self.is_item_allowed(item.get('link_to'), item.get('link_type')): - return True - - def _in_active_domains(item): - if not item.restrict_to_domain: - return True - else: - return item.restrict_to_domain in frappe.get_active_domains() - - for item in shortcuts: - if self.is_item_allowed(item.link_to, item.type) and _in_active_domains(item): - return True - - if not shortcuts and not self.doc.links: - return True - - return False - def is_permitted(self): """Returns true if Has Role is not set or the user is allowed.""" from frappe.utils import has_common @@ -346,20 +321,20 @@ def get_desktop_page(page): dict: dictionary of cards, charts and shortcuts to be displayed on website """ try: - wspace = Workspace(loads(page)) - wspace.build_workspace() + workspace = Workspace(loads(page)) + workspace.build_workspace() return { - 'charts': wspace.charts, - 'shortcuts': wspace.shortcuts, - 'cards': wspace.cards, - 'onboardings': wspace.onboardings + 'charts': workspace.charts, + 'shortcuts': workspace.shortcuts, + 'cards': workspace.cards, + 'onboardings': workspace.onboardings } except DoesNotExistError: frappe.log_error(frappe.get_traceback()) return {} @frappe.whitelist() -def get_wspace_sidebar_items(): +def get_workspace_sidebar_items(): """Get list of sidebar items for desk""" has_access = "Workspace Manager" in frappe.get_roles() @@ -385,8 +360,8 @@ def get_wspace_sidebar_items(): # Filter Page based on Permission for page in all_pages: try: - wspace = Workspace(page, True) - if wspace.is_permitted() and wspace.is_page_allowed() or has_access: + workspace = Workspace(page, True) + if has_access or workspace.is_permitted(): if page.public: pages.append(page) elif page.for_user == frappe.session.user: @@ -453,25 +428,24 @@ def get_custom_report_list(module): return out def save_new_widget(doc, page, blocks, new_widgets): + if loads(new_widgets): + widgets = _dict(loads(new_widgets)) - widgets = _dict(loads(new_widgets)) - - if widgets.chart: - doc.charts.extend(new_widget(widgets.chart, "Workspace Chart", "charts")) - if widgets.shortcut: - doc.shortcuts.extend(new_widget(widgets.shortcut, "Workspace Shortcut", "shortcuts")) - if widgets.card: - doc.build_links_table_from_card(widgets.card) + if widgets.chart: + doc.charts.extend(new_widget(widgets.chart, "Workspace Chart", "charts")) + if widgets.shortcut: + doc.shortcuts.extend(new_widget(widgets.shortcut, "Workspace Shortcut", "shortcuts")) + if widgets.card: + doc.build_links_table_from_card(widgets.card) # remove duplicate and unwanted widgets - if widgets: - clean_up(doc, blocks) + clean_up(doc, blocks) try: doc.save(ignore_permissions=True) except (ValidationError, TypeError) as e: # Create a json string to log - json_config = dumps(widgets, sort_keys=True, indent=4) + json_config = widgets and dumps(widgets, sort_keys=True, indent=4) # Error log body log = \ diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json index 04975c69e3..211029dfcf 100644 --- a/frappe/desk/doctype/workspace/workspace.json +++ b/frappe/desk/doctype/workspace/workspace.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_rename": 1, "autoname": "field:label", "beta": 1, "creation": "2020-01-23 13:45:59.470592", @@ -141,7 +142,7 @@ }, { "fieldname": "sequence_id", - "fieldtype": "Int", + "fieldtype": "Float", "label": "Sequence Id" }, { @@ -158,7 +159,7 @@ ], "in_create": 1, "links": [], - "modified": "2021-09-16 12:01:06.450622", + "modified": "2021-12-15 19:33:00.805265", "modified_by": "Administrator", "module": "Desk", "name": "Workspace", diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index 94114e3918..b40f517350 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -6,6 +6,7 @@ import frappe from frappe import _ from frappe.modules.export_file import export_to_files from frappe.model.document import Document +from frappe.model.rename_doc import rename_doc from frappe.desk.desktop import save_new_widget from frappe.desk.utils import validate_route_conflict @@ -121,77 +122,157 @@ def get_report_type(report): report_type = frappe.get_value("Report", report, "report_type") return report_type in ["Query Report", "Script Report", "Custom Report"] +@frappe.whitelist() +def new_page(new_page): + if not loads(new_page): + return + + page = loads(new_page) + + if page.get("public") and not is_workspace_manager(): + return + + doc = frappe.new_doc('Workspace') + doc.title = page.get('title') + doc.icon = page.get('icon') + doc.content = page.get('content') + doc.parent_page = page.get('parent_page') + doc.label = page.get('label') + doc.for_user = page.get('for_user') + doc.public = page.get('public') + doc.sequence_id = last_sequence_id(doc) + 1 + doc.save(ignore_permissions=True) + + return doc @frappe.whitelist() -def save_page(title, icon, parent, public, sb_public_items, sb_private_items, deleted_pages, new_widgets, blocks, save): - save = frappe.parse_json(save) +def save_page(title, public, new_widgets, blocks): public = frappe.parse_json(public) - if save: - doc = frappe.new_doc('Workspace') + + filters = { + 'public': public, + 'label': title + } + + if not public: + filters = { + 'for_user': frappe.session.user, + 'label': title + "-" + frappe.session.user + } + pages = frappe.get_list("Workspace", filters=filters) + if pages: + doc = frappe.get_doc("Workspace", pages[0]) + + doc.content = blocks + doc.save(ignore_permissions=True) + + save_new_widget(doc, title, blocks, new_widgets) + + return {"name": title, "public": public, "label": doc.label} + +@frappe.whitelist() +def update_page(name, title, icon, parent, public): + public = frappe.parse_json(public) + + doc = frappe.get_doc("Workspace", name) + + filters = { + 'parent_page': doc.title, + 'public': doc.public + } + child_docs = frappe.get_list("Workspace", filters=filters) + + if doc: doc.title = title doc.icon = icon - doc.content = blocks doc.parent_page = parent - - if public: - doc.label = title - doc.public = 1 - else: - doc.label = title + "-" + frappe.session.user - doc.for_user = frappe.session.user - doc.save(ignore_permissions=True) - else: - if public: - filters = { - 'public': public, - 'label': title - } - else: - filters = { - 'for_user': frappe.session.user, - 'label': title + "-" + frappe.session.user - } - pages = frappe.get_list("Workspace", filters=filters) - if pages: - doc = frappe.get_doc("Workspace", pages[0]) - - doc.content = blocks + if doc.public != public: + doc.sequence_id = frappe.db.count('Workspace', {'public':public}, cache=True) + doc.public = public + doc.for_user = '' if public else doc.for_user or frappe.session.user + doc.label = '{0}-{1}'.format(title, doc.for_user) if doc.for_user else title doc.save(ignore_permissions=True) - if loads(new_widgets): - save_new_widget(doc, title, blocks, new_widgets) + if name != doc.label: + rename_doc("Workspace", name, doc.label, force=True, ignore_permissions=True) - if loads(sb_public_items) or loads(sb_private_items): - sort_pages(loads(sb_public_items), loads(sb_private_items)) + # update new name and public in child pages + if child_docs: + for child in child_docs: + child_doc = frappe.get_doc("Workspace", child.name) + child_doc.parent_page = doc.title + child_doc.public = doc.public + child_doc.save(ignore_permissions=True) - if loads(deleted_pages): - return delete_pages(loads(deleted_pages)) + return {"name": doc.title, "public": doc.public, "label": doc.label} - return {"name": title, "public": public, "label": doc.label} +@frappe.whitelist() +def duplicate_page(page_name, new_page): + if not loads(new_page): + return + + new_page = loads(new_page) + + if new_page.get("is_public") and not is_workspace_manager(): + return + + old_doc = frappe.get_doc("Workspace", page_name) + doc = frappe.copy_doc(old_doc) + doc.title = new_page.get('title') + doc.icon = new_page.get('icon') + doc.parent_page = new_page.get('parent') or '' + doc.public = new_page.get('is_public') + doc.for_user = '' + doc.label = doc.title + if not doc.public: + doc.for_user = doc.for_user or frappe.session.user + doc.label = '{0}-{1}'.format(doc.title, doc.for_user) + doc.name = doc.label + if old_doc.public == doc.public: + doc.sequence_id += 0.1 + else: + doc.sequence_id = last_sequence_id(doc) + 1 + doc.insert(ignore_permissions=True) -def delete_pages(deleted_pages): - for page in deleted_pages: - if page.get("public") and not is_workspace_manager(): - return {"name": page.get("title"), "public": 1, "label": page.get("label")} + return doc - if frappe.db.exists("Workspace", page.get("name")): - frappe.get_doc("Workspace", page.get("name")).delete(ignore_permissions=True) +@frappe.whitelist() +def delete_page(page): + if not loads(page): + return + + page = loads(page) + + if page.get("public") and not is_workspace_manager(): + return + + if frappe.db.exists("Workspace", page.get("name")): + frappe.get_doc("Workspace", page.get("name")).delete(ignore_permissions=True) - return {"name": "Home", "public": 1, "label": "Home"} + return {"name": page.get("name"), "public": page.get("public"), "title": page.get("title")} +@frappe.whitelist() def sort_pages(sb_public_items, sb_private_items): - wspace_public_pages = get_page_list(['name', 'title'], {'public': 1}) - wspace_private_pages = get_page_list(['name', 'title'], {'for_user': frappe.session.user}) + if not loads(sb_public_items) and not loads(sb_private_items): + return + + sb_public_items = loads(sb_public_items) + sb_private_items = loads(sb_private_items) + + workspace_public_pages = get_page_list(['name', 'title'], {'public': 1}) + workspace_private_pages = get_page_list(['name', 'title'], {'for_user': frappe.session.user}) if sb_private_items: - sort_page(wspace_private_pages, sb_private_items) + return sort_page(workspace_private_pages, sb_private_items) if sb_public_items and is_workspace_manager(): - sort_page(wspace_public_pages, sb_public_items) + return sort_page(workspace_public_pages, sb_public_items) -def sort_page(wspace_pages, pages): + return False + +def sort_page(workspace_pages, pages): for seq, d in enumerate(pages): - for page in wspace_pages: + for page in workspace_pages: if page.title == d.get('title'): doc = frappe.get_doc('Workspace', page.name) doc.sequence_id = seq + 1 @@ -199,6 +280,27 @@ def sort_page(wspace_pages, pages): doc.save(ignore_permissions=True) break + return True + +def last_sequence_id(doc): + doc_exists = frappe.db.exists({ + 'doctype': 'Workspace', + 'public': doc.public, + 'for_user': doc.for_user + }) + + if not doc_exists: + return 0 + + return frappe.db.get_list('Workspace', + fields=['sequence_id'], + filters={ + 'public': doc.public, + 'for_user': doc.for_user + }, + order_by="sequence_id desc" + )[0].sequence_id + def get_page_list(fields, filters): return frappe.get_list("Workspace", fields=fields, filters=filters, order_by='sequence_id asc') diff --git a/frappe/desk/moduleview.py b/frappe/desk/moduleview.py index e2e2c4c155..7a9c211c3c 100644 --- a/frappe/desk/moduleview.py +++ b/frappe/desk/moduleview.py @@ -524,7 +524,7 @@ def get_last_modified(doctype): raise # hack: save as -1 so that it is cached - if last_modified==None: + if last_modified is None: last_modified = -1 return last_modified diff --git a/frappe/desk/page/user_profile/user_profile.py b/frappe/desk/page/user_profile/user_profile.py index 73df6d78cb..0d91fd0d91 100644 --- a/frappe/desk/page/user_profile/user_profile.py +++ b/frappe/desk/page/user_profile/user_profile.py @@ -30,7 +30,7 @@ def get_energy_points_percentage_chart_data(user, field): as_list = True) return { - "labels": [r[0] for r in result if r[0] != None], + "labels": [r[0] for r in result if r[0] is not None], "datasets": [{ "values": [r[1] for r in result] }] diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index e81ed0767b..27ac882016 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -19,7 +19,7 @@ from frappe.utils import add_user_info def get(): args = get_form_params() # If virtual doctype get data from controller het_list method - if frappe.db.get_value("DocType", filters={"name": args.doctype}, fieldname="is_virtual"): + if is_virtual_doctype(args.doctype): controller = get_controller(args.doctype) data = compress(controller(args.doctype).get_list(args)) else: @@ -29,17 +29,31 @@ def get(): @frappe.whitelist() @frappe.read_only() def get_list(): - # uncompressed (refactored from frappe.model.db_query.get_list) - return execute(**get_form_params()) + args = get_form_params() + + if is_virtual_doctype(args.doctype): + controller = get_controller(args.doctype) + data = controller(args.doctype).get_list(args) + else: + # uncompressed (refactored from frappe.model.db_query.get_list) + data = execute(**args) + + return data @frappe.whitelist() @frappe.read_only() def get_count(): args = get_form_params() - distinct = 'distinct ' if args.distinct=='true' else '' - args.fields = [f"count({distinct}`tab{args.doctype}`.name) as total_count"] - return execute(**args)[0].get('total_count') + if is_virtual_doctype(args.doctype): + controller = get_controller(args.doctype) + data = controller(args.doctype).get_count(args) + else: + distinct = 'distinct ' if args.distinct=='true' else '' + args.fields = [f"count({distinct}`tab{args.doctype}`.name) as total_count"] + data = execute(**args)[0].get('total_count') + + return data def execute(doctype, *args, **kwargs): return DatabaseQuery(doctype).execute(*args, **kwargs) @@ -438,7 +452,14 @@ def get_sidebar_stats(stats, doctype, filters=None): if filters is None: filters = [] - return {"stats": get_stats(stats, doctype, filters)} + if is_virtual_doctype(doctype): + controller = get_controller(doctype) + args = {"stats": stats, "filters": filters} + data = controller(doctype).get_stats(args) + else: + data = get_stats(stats, doctype, filters) + + return {"stats": data} @frappe.whitelist() @frappe.read_only() @@ -560,7 +581,7 @@ def get_match_cond(doctype, as_condition=True): return ((' and ' + cond) if cond else "").replace("%", "%%") def build_match_conditions(doctype, user=None, as_condition=True): - match_conditions = DatabaseQuery(doctype, user=user).build_match_conditions(as_condition=as_condition) + match_conditions = DatabaseQuery(doctype, user=user).build_match_conditions(as_condition=as_condition) if as_condition: return match_conditions.replace("%", "%%") else: @@ -598,3 +619,7 @@ def get_filters_cond(doctype, filters, conditions, ignore_permissions=None, with else: cond = '' return cond + +def is_virtual_doctype(doctype): + return frappe.db.get_value("DocType", doctype, "is_virtual") + diff --git a/frappe/desk/search.py b/frappe/desk/search.py index db88e6ec52..95397070ae 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -107,7 +107,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, else: filters.append([doctype, f[0], "=", f[1]]) - if filters==None: + if filters is None: filters = [] or_filters = [] diff --git a/frappe/installer.py b/frappe/installer.py old mode 100755 new mode 100644 index b50fa4a3b5..d892ff4ddc --- a/frappe/installer.py +++ b/frappe/installer.py @@ -154,7 +154,7 @@ def install_app(name, verbose=False, set_as_patched=True): for before_install in app_hooks.before_install or []: out = frappe.get_attr(before_install)() - if out==False: + if out is False: return if name != "frappe": @@ -346,14 +346,15 @@ def post_install(rebuild_website=False): def set_all_patches_as_completed(app): - patch_path = os.path.join(frappe.get_pymodule_path(app), "patches.txt") - if os.path.exists(patch_path): - for patch in frappe.get_file_items(patch_path): - frappe.get_doc({ - "doctype": "Patch Log", - "patch": patch - }).insert(ignore_permissions=True) - frappe.db.commit() + from frappe.modules.patch_handler import get_patches_from_app + + patches = get_patches_from_app(app) + for patch in patches: + frappe.get_doc({ + "doctype": "Patch Log", + "patch": patch + }).insert(ignore_permissions=True) + frappe.db.commit() def init_singles(): diff --git a/frappe/integrations/offsite_backup_utils.py b/frappe/integrations/offsite_backup_utils.py index 416d656d90..4242676d94 100644 --- a/frappe/integrations/offsite_backup_utils.py +++ b/frappe/integrations/offsite_backup_utils.py @@ -65,10 +65,7 @@ def get_latest_backup_file(with_files=False): return database, config -def get_file_size(file_path, unit): - if not unit: - unit = "MB" - +def get_file_size(file_path, unit='MB'): file_size = os.path.getsize(file_path) memory_size_unit_mapper = {"KB": 1, "MB": 2, "GB": 3, "TB": 4} @@ -99,7 +96,7 @@ def get_chunk_site(file_size): def validate_file_size(): frappe.flags.create_new_backup = True latest_file, site_config = get_latest_backup_file() - file_size = get_file_size(latest_file, unit="GB") + file_size = get_file_size(latest_file, unit="GB") if latest_file else 0 if file_size > 1: frappe.flags.create_new_backup = False diff --git a/frappe/integrations/workspace/integrations/integrations.json b/frappe/integrations/workspace/integrations/integrations.json index b85056e3ef..bbd2e1199f 100644 --- a/frappe/integrations/workspace/integrations/integrations.json +++ b/frappe/integrations/workspace/integrations/integrations.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Backup\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Google Services\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Authentication\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Payments\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Backup\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Google Services\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Authentication\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Payments\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", "creation": "2020-03-02 15:16:18.714190", "docstatus": 0, "doctype": "Workspace", @@ -260,7 +260,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:16:00.355268", + "modified": "2022-01-13 17:39:01.292154", "modified_by": "Administrator", "module": "Integrations", "name": "Integrations", @@ -269,7 +269,7 @@ "public": 1, "restrict_to_domain": "", "roles": [], - "sequence_id": 15, + "sequence_id": 15.0, "shortcuts": [], "title": "Integrations" } \ No newline at end of file diff --git a/frappe/migrate.py b/frappe/migrate.py index 6abc38796f..d13fe858f7 100644 --- a/frappe/migrate.py +++ b/frappe/migrate.py @@ -19,6 +19,8 @@ from frappe.modules.utils import sync_customizations from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs from frappe.search.website_search import build_index_for_all_routes from frappe.database.schema import add_column +from frappe.modules.patch_handler import PatchType + def migrate(verbose=True, skip_failing=False, skip_search_index=False): @@ -59,16 +61,13 @@ Otherwise, check the server logs and ensure that all the required services are r clear_global_cache() - #run before_migrate hooks for app in frappe.get_installed_apps(): for fn in frappe.get_hooks('before_migrate', app_name=app): frappe.get_attr(fn)() - # run patches - frappe.modules.patch_handler.run_all(skip_failing) - - # sync + frappe.modules.patch_handler.run_all(skip_failing=skip_failing, patch_type=PatchType.pre_model_sync) frappe.model.sync.sync_all() + frappe.modules.patch_handler.run_all(skip_failing=skip_failing, patch_type=PatchType.post_model_sync) frappe.translate.clear_cache() sync_jobs() sync_fixtures() @@ -78,18 +77,16 @@ Otherwise, check the server logs and ensure that all the required services are r frappe.get_doc('Portal Settings', 'Portal Settings').sync_menu() - # syncs statics + # syncs static files clear_website_cache() # updating installed applications data frappe.get_single('Installed Applications').update_versions() - #run after_migrate hooks for app in frappe.get_installed_apps(): for fn in frappe.get_hooks('after_migrate', app_name=app): frappe.get_attr(fn)() - # build web_routes index if not skip_search_index: # Run this last as it updates the current session print('Building search index for {}'.format(frappe.local.site)) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 631174b4db..11e97a38b9 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -172,7 +172,7 @@ class BaseDocument(object): ... }) """ - if value==None: + if value is None: value={} if isinstance(value, (dict, BaseDocument)): if not self.__dict__.get(key): @@ -272,7 +272,7 @@ class BaseDocument(object): )): d[fieldname] = str(d[fieldname]) - if d[fieldname] == None and ignore_nulls: + if d[fieldname] is None and ignore_nulls: del d[fieldname] return d diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 51d53c69a5..79be261981 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -545,7 +545,7 @@ class DatabaseQuery(object): elif f.operator.lower() in ("like", "not like") or (isinstance(f.value, str) and (not df or df.fieldtype not in ["Float", "Int", "Currency", "Percent", "Check"])): - value = "" if f.value==None else f.value + value = "" if f.value is None else f.value fallback = "''" if f.operator.lower() in ("like", "not like") and isinstance(value, str): diff --git a/frappe/model/document.py b/frappe/model/document.py index e055a12950..db4b7703ba 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -211,13 +211,13 @@ class Document(BaseDocument): self.flags.notifications_executed = [] - if ignore_permissions!=None: + if ignore_permissions is not None: self.flags.ignore_permissions = ignore_permissions - if ignore_links!=None: + if ignore_links is not None: self.flags.ignore_links = ignore_links - if ignore_mandatory!=None: + if ignore_mandatory is not None: self.flags.ignore_mandatory = ignore_mandatory self.set("__islocal", True) @@ -297,7 +297,7 @@ class Document(BaseDocument): self.flags.notifications_executed = [] - if ignore_permissions!=None: + if ignore_permissions is not None: self.flags.ignore_permissions = ignore_permissions self.flags.ignore_version = frappe.flags.in_test if ignore_version is None else ignore_version @@ -441,7 +441,7 @@ class Document(BaseDocument): values = self.as_dict() # format values for key, value in values.items(): - if value==None: + if value is None: values[key] = "" return values @@ -489,7 +489,7 @@ class Document(BaseDocument): frappe.flags.currently_saving.append((self.doctype, self.name)) def set_docstatus(self): - if self.docstatus==None: + if self.docstatus is None: self.docstatus=0 for d in self.get_all_children(): @@ -887,14 +887,14 @@ class Document(BaseDocument): if (frappe.flags.in_import and frappe.flags.mute_emails) or frappe.flags.in_patch or frappe.flags.in_install: return - if self.flags.notifications_executed==None: + if self.flags.notifications_executed is None: self.flags.notifications_executed = [] from frappe.email.doctype.notification.notification import evaluate_alert - if self.flags.notifications == None: + if self.flags.notifications is None: alerts = frappe.cache().hget('notifications', self.doctype) - if alerts==None: + if alerts is None: alerts = frappe.get_all('Notification', fields=['name', 'event', 'method'], filters={'enabled': 1, 'document_type': self.doctype}) frappe.cache().hset('notifications', self.doctype, alerts) diff --git a/frappe/model/dynamic_links.py b/frappe/model/dynamic_links.py index 7311b39b30..03f616ef60 100644 --- a/frappe/model/dynamic_links.py +++ b/frappe/model/dynamic_links.py @@ -32,7 +32,7 @@ def get_dynamic_link_map(for_delete=False): Note: Will not map single doctypes ''' - if getattr(frappe.local, 'dynamic_link_map', None)==None or frappe.flags.in_test: + if getattr(frappe.local, 'dynamic_link_map', None) is None or frappe.flags.in_test: # Build from scratch dynamic_link_map = {} for df in get_dynamic_links(): diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 2cc5818414..6ffaadc5eb 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -80,6 +80,7 @@ def rename_doc( if doctype=='DocType': rename_doctype(doctype, old, new, force) + update_customizations(old, new) update_attachments(doctype, old, new) @@ -174,6 +175,8 @@ def update_user_settings(old, new, link_fields): else: continue +def update_customizations(old: str, new: str) -> None: + frappe.db.set_value("Custom DocPerm", {"parent": old}, "parent", new, update_modified=False) def update_attachments(doctype, old, new): try: diff --git a/frappe/modules/patch_handler.py b/frappe/modules/patch_handler.py index 8dfb27c0b8..7b635ac940 100644 --- a/frappe/modules/patch_handler.py +++ b/frappe/modules/patch_handler.py @@ -1,37 +1,76 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE -""" - Execute Patch Files +""" Patch Handler. + +This file manages execution of manaully written patches. Patches are script +that apply changes in database schema or data to accomodate for changes in the +code. + +Ways to specify patches: + +1. patches.txt file specifies patches that run before doctype schema +migration. Each line represents one patch (old format). +2. patches.txt can alternatively also separate pre and post model sync +patches by using INI like file format: + ```patches.txt + [pre_model_sync] + app.module.patch1 + app.module.patch2 + + + [post_model_sync] + app.module.patch3 + ``` - To run directly + When different sections are specified patches are executed in this order: + 1. Run pre_model_sync patches + 2. Reload/resync all doctype schema + 3. Run post_model_sync patches - python lib/wnf.py patch patch1, patch2 etc - python lib/wnf.py patch -f patch1, patch2 etc + Hence any patch that just needs to modify data but doesn't depend on + old schema should be added to post_model_sync section of file. - where patch1, patch2 is module name +3. simple python commands can be added by starting line with `execute:` +`execute:` example: `execute:print("hello world")` """ -import frappe, frappe.permissions, time -class PatchError(Exception): pass +import configparser +import time +from enum import Enum +from typing import List, Optional + +import frappe + + +class PatchError(Exception): + pass + + +class PatchType(Enum): + pre_model_sync = "pre_model_sync" + post_model_sync = "post_model_sync" -def run_all(skip_failing=False): + +def run_all(skip_failing: bool = False, patch_type: Optional[PatchType] = None) -> None: """run all pending patches""" - executed = [p[0] for p in frappe.db.sql("""select patch from `tabPatch Log`""")] + executed = set(frappe.get_all("Patch Log", fields="patch", pluck="patch")) frappe.flags.final_patches = [] def run_patch(patch): try: if not run_single(patchmodule = patch): - log(patch + ': failed: STOPPED') + print(patch + ': failed: STOPPED') raise PatchError(patch) except Exception: if not skip_failing: raise else: - log('Failed to execute patch') + print('Failed to execute patch') + + patches = get_all_patches(patch_type=patch_type) - for patch in get_all_patches(): + for patch in patches: if patch and (patch not in executed): run_patch(patch) @@ -40,18 +79,57 @@ def run_all(skip_failing=False): patch = patch.replace('finally:', '') run_patch(patch) -def get_all_patches(): +def get_all_patches(patch_type: Optional[PatchType] = None) -> List[str]: + + if patch_type and not isinstance(patch_type, PatchType): + frappe.throw(f"Unsupported patch type specified: {patch_type}") + patches = [] for app in frappe.get_installed_apps(): - if app == "shopping_cart": - continue - # 3-to-4 fix - if app=="webnotes": - app="frappe" - patches.extend(frappe.get_file_items(frappe.get_pymodule_path(app, "patches.txt"))) + patches.extend(get_patches_from_app(app, patch_type=patch_type)) return patches +def get_patches_from_app(app: str, patch_type: Optional[PatchType] = None) -> List[str]: + """ Get patches from an app's patches.txt + + patches.txt can be: + 1. ini like file with section for different patch_type + 2. plain text file with each line representing a patch. + """ + + patches_txt = frappe.get_pymodule_path(app, "patches.txt") + + try: + # Attempt to parse as ini file with pre/post patches + # allow_no_value: patches are not key value pairs + # delimiters = '\n' to avoid treating default `:` and `=` in execute as k:v delimiter + parser = configparser.ConfigParser(allow_no_value=True, delimiters="\n") + # preserve case + parser.optionxform = str + parser.read(patches_txt) + + # empty file + if not parser.sections(): + return [] + + if not patch_type: + return [patch for patch in parser[PatchType.pre_model_sync.value]] + \ + [patch for patch in parser[PatchType.post_model_sync.value]] + + if patch_type.value in parser.sections(): + return [patch for patch in parser[patch_type.value]] + else: + frappe.throw(frappe._("Patch type {} not found in patches.txt").format(patch_type)) + + except configparser.MissingSectionHeaderError: + # treat as old format with each line representing a single patch + # backward compatbility with old patches.txt format + if not patch_type or patch_type == PatchType.pre_model_sync: + return frappe.get_file_items(patches_txt) + + return [] + def reload_doc(args): import frappe.modules run_single(method = frappe.modules.reload_doc, methodargs = args) @@ -73,7 +151,7 @@ def execute_patch(patchmodule, method=None, methodargs=None): frappe.db.begin() start_time = time.time() try: - log('Executing {patch} in {site} ({db})'.format(patch=patchmodule or str(methodargs), + print('Executing {patch} in {site} ({db})'.format(patch=patchmodule or str(methodargs), site=frappe.local.site, db=frappe.db.cur_db_name)) if patchmodule: if patchmodule.startswith("finally:"): @@ -96,7 +174,7 @@ def execute_patch(patchmodule, method=None, methodargs=None): frappe.db.commit() end_time = time.time() block_user(False) - log('Success: Done in {time}s'.format(time = round(end_time - start_time, 3))) + print('Success: Done in {time}s'.format(time = round(end_time - start_time, 3))) return True @@ -109,10 +187,7 @@ def executed(patchmodule): if patchmodule.startswith('finally:'): # patches are saved without the finally: tag patchmodule = patchmodule.replace('finally:', '') - done = frappe.db.get_value("Patch Log", {"patch": patchmodule}) - # if done: - # print "Patch %s already executed in %s" % (patchmodule, frappe.db.cur_db_name) - return done + return frappe.db.get_value("Patch Log", {"patch": patchmodule}) def block_user(block, msg=None): """stop/start execution till patch is run""" @@ -128,6 +203,3 @@ def check_session_stopped(): if frappe.db.get_global("__session_status")=='stop': frappe.msgprint(frappe.db.get_global("__session_status_message")) raise frappe.SessionStopped('Session Stopped') - -def log(msg): - print (msg) diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py index bbfd63a277..13b52d2020 100644 --- a/frappe/modules/utils.py +++ b/frappe/modules/utils.py @@ -257,6 +257,12 @@ def make_boilerplate(template, doc, opts=None): pass def get_list(self, args): + pass + + def get_count(self, args): + pass + + def get_stats(self, args): pass""" with open(target_file_path, 'w') as target: diff --git a/frappe/patches.txt b/frappe/patches.txt index 4231a59466..c393b456e3 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -1,3 +1,4 @@ +[pre_model_sync] frappe.patches.v12_0.remove_deprecated_fields_from_doctype #3 execute:frappe.utils.global_search.setup_global_search_table() execute:frappe.reload_doc('core', 'doctype', 'doctype_action', force=True) #2019-09-23 @@ -87,7 +88,6 @@ frappe.patches.v11_0.set_missing_creation_and_modified_value_for_user_permission frappe.patches.v11_0.set_default_letter_head_source frappe.patches.v12_0.set_primary_key_in_series execute:frappe.delete_doc("Page", "modules", ignore_missing=True) -frappe.patches.v11_0.set_default_letter_head_source frappe.patches.v12_0.setup_comments_from_communications frappe.patches.v12_0.replace_null_values_in_tables frappe.patches.v12_0.reset_home_settings @@ -123,6 +123,8 @@ frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats frappe.patches.v12_0.remove_example_email_thread_notify execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders() frappe.patches.v12_0.set_correct_url_in_files +execute:frappe.reload_doc('core', 'doctype', 'doctype') +execute:frappe.reload_doc('custom', 'doctype', 'property_setter') frappe.patches.v13_0.remove_invalid_options_for_data_fields frappe.patches.v13_0.website_theme_custom_scss frappe.patches.v13_0.make_user_type @@ -154,7 +156,6 @@ frappe.patches.v13_0.rename_notification_fields frappe.patches.v13_0.remove_duplicate_navbar_items frappe.patches.v13_0.set_social_icons frappe.patches.v12_0.set_default_password_reset_limit -execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) frappe.patches.v13_0.set_route_for_blog_category frappe.patches.v13_0.enable_custom_script frappe.patches.v13_0.update_newsletter_content_type @@ -180,16 +181,17 @@ 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.v13_0.set_first_day_of_the_week -frappe.patches.v14_0.drop_data_import_legacy frappe.patches.v14_0.rename_cancelled_documents -frappe.patches.v14_0.copy_mail_data #08.03.21 frappe.patches.v14_0.update_workspace2 # 20.09.2021 +frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021 +frappe.patches.v14_0.transform_todo_schema + +[post_model_sync] +frappe.patches.v14_0.drop_data_import_legacy +frappe.patches.v14_0.copy_mail_data #08.03.21 frappe.patches.v14_0.update_github_endpoints #08-11-2021 frappe.patches.v14_0.remove_db_aggregation -frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021 frappe.patches.v14_0.update_color_names_in_kanban_board_column -frappe.patches.v14_0.transform_todo_schema diff --git a/frappe/patches/v11_0/remove_skip_for_doctype.py b/frappe/patches/v11_0/remove_skip_for_doctype.py index 1bbe74bb6d..6e66c75f68 100644 --- a/frappe/patches/v11_0/remove_skip_for_doctype.py +++ b/frappe/patches/v11_0/remove_skip_for_doctype.py @@ -33,7 +33,7 @@ def execute(): continue skip_for_doctype = user_permission.skip_for_doctype.split('\n') else: # while migrating from v10 -> v11 - if skip_for_doctype_map.get((user_permission.allow, user_permission.user)) == None: + if skip_for_doctype_map.get((user_permission.allow, user_permission.user)) is None: skip_for_doctype = get_doctypes_to_skip(user_permission.allow, user_permission.user) # cache skip for doctype for same user and doctype skip_for_doctype_map[(user_permission.allow, user_permission.user)] = skip_for_doctype diff --git a/frappe/patches/v14_0/copy_mail_data.py b/frappe/patches/v14_0/copy_mail_data.py index d3a5c59209..8ef9cfaf1f 100644 --- a/frappe/patches/v14_0/copy_mail_data.py +++ b/frappe/patches/v14_0/copy_mail_data.py @@ -3,9 +3,6 @@ import frappe def execute(): - frappe.reload_doc("email", "doctype", "imap_folder") - frappe.reload_doc("email", "doctype", "email_account") - # patch for all Email Account with the flag use_imap for email_account in frappe.get_list("Email Account", filters={"enable_incoming": 1, "use_imap": 1}): # get all data from Email Account diff --git a/frappe/patches/v14_0/update_color_names_in_kanban_board_column.py b/frappe/patches/v14_0/update_color_names_in_kanban_board_column.py index ea8a10e43a..ff03604754 100644 --- a/frappe/patches/v14_0/update_color_names_in_kanban_board_column.py +++ b/frappe/patches/v14_0/update_color_names_in_kanban_board_column.py @@ -5,7 +5,6 @@ from __future__ import unicode_literals import frappe def execute(): - frappe.reload_doc("desk", "doctype", "kanban_board_column") indicator_map = { 'blue': 'Blue', 'orange': 'Orange', diff --git a/frappe/patches/v14_0/update_workspace2.py b/frappe/patches/v14_0/update_workspace2.py index 82076c4328..a4b057b989 100644 --- a/frappe/patches/v14_0/update_workspace2.py +++ b/frappe/patches/v14_0/update_workspace2.py @@ -5,10 +5,10 @@ from frappe import _ def execute(): frappe.reload_doc('desk', 'doctype', 'workspace', force=True) - for seq, wspace in enumerate(frappe.get_all('Workspace', order_by='name asc')): - doc = frappe.get_doc('Workspace', wspace.name) + for seq, workspace in enumerate(frappe.get_all('Workspace', order_by='name asc')): + doc = frappe.get_doc('Workspace', workspace.name) content = create_content(doc) - update_wspace(doc, seq, content) + update_workspace(doc, seq, content) frappe.db.commit() def create_content(doc): @@ -49,7 +49,7 @@ def create_content(doc): del doc.links[doc.links.index(l)] return content -def update_wspace(doc, seq, content): +def update_workspace(doc, seq, content): if not doc.title and not doc.content and not doc.is_standard and not doc.public: doc.sequence_id = seq + 1 doc.content = json.dumps(content) diff --git a/frappe/permissions.py b/frappe/permissions.py index 5faaf7dcfb..af17faba01 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -23,7 +23,7 @@ def print_has_permission_check_logs(func): 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 - raise_exception = False if kwargs.get('raise_exception') == False else True + raise_exception = False if kwargs.get('raise_exception') is False else True # print only if access denied # and if user is checking his own permission @@ -559,7 +559,9 @@ def filter_allowed_docs_for_doctype(user_permissions, doctype, with_default_doc= return (allowed_doc, default_doc) if with_default_doc else allowed_doc def push_perm_check_log(log): - if frappe.flags.get('has_permission_check_logs') == None: return + if frappe.flags.get('has_permission_check_logs') is None: + return + frappe.flags.get('has_permission_check_logs').append(_(log)) def has_child_table_permission(child_doctype, ptype="read", child_doc=None, diff --git a/frappe/public/icons/timeless/symbol-defs.svg b/frappe/public/icons/timeless/symbol-defs.svg index b878f713e9..f2977e3016 100644 --- a/frappe/public/icons/timeless/symbol-defs.svg +++ b/frappe/public/icons/timeless/symbol-defs.svg @@ -1,217 +1,262 @@ diff --git a/frappe/public/js/desk.bundle.js b/frappe/public/js/desk.bundle.js index cac02c7a68..e056a34be2 100644 --- a/frappe/public/js/desk.bundle.js +++ b/frappe/public/js/desk.bundle.js @@ -96,6 +96,7 @@ import "./frappe/ui/sort_selector.js"; import "./frappe/change_log.html"; import "./frappe/ui/workspace_loading_skeleton.html"; +import "./frappe/ui/workspace_sidebar_loading_skeleton.html"; import "./frappe/desk.js"; import "./frappe/query_string.js"; diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 202cee645a..51ada70948 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -214,19 +214,20 @@ frappe.Application = class Application { email_password_prompt(email_account,user,i) { var me = this; + const email_id = email_account[i]["email_id"]; let d = new frappe.ui.Dialog({ title: __('Password missing in Email Account'), fields: [ { 'fieldname': 'password', 'fieldtype': 'Password', - 'label': __('Please enter the password for: {0}', [email_account[i]["email_id"]]), + 'label': __('Please enter the password for: {0}', [email_id], "Email Account"), 'reqd': 1 }, { "fieldname": "submit", "fieldtype": "Button", - "label": __("Submit") + "label": __("Submit", null, "Submit password for Email Account") } ] }); diff --git a/frappe/public/js/frappe/form/controls/date_range.js b/frappe/public/js/frappe/form/controls/date_range.js index 727e9d55c2..170404f575 100644 --- a/frappe/public/js/frappe/form/controls/date_range.js +++ b/frappe/public/js/frappe/form/controls/date_range.js @@ -11,7 +11,8 @@ frappe.ui.form.ControlDateRange = class ControlDateRange extends frappe.ui.form. language: "en", range: true, autoClose: true, - toggleSelected: false + toggleSelected: false, + firstDay: frappe.datetime.get_first_day_of_the_week_index() }; this.datepicker_options.dateFormat = (frappe.boot.sysdefaults.date_format || 'yyyy-mm-dd'); diff --git a/frappe/public/js/frappe/form/controls/dynamic_link.js b/frappe/public/js/frappe/form/controls/dynamic_link.js index 2c5661ca87..ea9ceb35f3 100644 --- a/frappe/public/js/frappe/form/controls/dynamic_link.js +++ b/frappe/public/js/frappe/form/controls/dynamic_link.js @@ -2,7 +2,7 @@ frappe.ui.form.ControlDynamicLink = class ControlDynamicLink extends frappe.ui.f get_options() { let options = ''; if (this.df.get_options) { - options = this.df.get_options(); + options = this.df.get_options(this); } else if (this.docname==null && cur_dialog) { //for dialog box options = cur_dialog.get_value(this.df.options); diff --git a/frappe/public/js/frappe/form/footer/base_timeline.js b/frappe/public/js/frappe/form/footer/base_timeline.js index beeba16459..ba7a4eb565 100644 --- a/frappe/public/js/frappe/form/footer/base_timeline.js +++ b/frappe/public/js/frappe/form/footer/base_timeline.js @@ -12,8 +12,11 @@ class BaseTimeline { this.wrapper = this.timeline_wrapper; this.timeline_items_wrapper = $(`
`); this.timeline_actions_wrapper = $(` -
-
+
+
+
+
+
`); @@ -37,7 +40,7 @@ class BaseTimeline { ${label} `); action_btn.click(action); - this.timeline_actions_wrapper.append(action_btn); + this.timeline_actions_wrapper.find('.action-buttons').append(action_btn); return action_btn; } diff --git a/frappe/public/js/frappe/form/footer/form_timeline.js b/frappe/public/js/frappe/form/footer/form_timeline.js index f278d1b64b..d440874f36 100644 --- a/frappe/public/js/frappe/form/footer/form_timeline.js +++ b/frappe/public/js/frappe/form/footer/form_timeline.js @@ -77,12 +77,14 @@ class FormTimeline extends BaseTimeline { const message = __("Add to this activity by mailing to {0}", [link.bold()]); this.document_email_link_wrapper = $(` -