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 1b7634d211..5808bd52ef 100644 --- a/cypress/integration/timeline_email.js +++ b/cypress/integration/timeline_email.js @@ -14,7 +14,7 @@ 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(); @@ -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(); 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 20fc41b83a..c6cbfead43 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -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) 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/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/communication/email.py b/frappe/core/doctype/communication/email.py index 54ddbce2c4..46ef7bf5d2 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -1,29 +1,32 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE -import frappe import json -from email.utils import formataddr -from frappe.core.utils import get_parent_doc -from frappe.utils import (get_url, get_formatted_email, cint, list_to_str, - validate_email_address, split_emails, parse_addr, get_datetime) -from frappe.email.email_body import get_message_id +from typing import TYPE_CHECKING, Dict + +import frappe import frappe.email.smtp -import time from frappe import _ -from frappe.utils.background_jobs import enqueue +from frappe.email.email_body import get_message_id +from frappe.utils import (cint, get_datetime, get_formatted_email, + list_to_str, split_emails, validate_email_address) + +if TYPE_CHECKING: + from frappe.core.doctype.communication.communication import Communication + OUTGOING_EMAIL_ACCOUNT_MISSING = _(""" Unable to send mail because of a missing email account. Please setup default Email Account from Setup > Email > Email Account """) + @frappe.whitelist() def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent", sender=None, sender_full_name=None, recipients=None, communication_medium="Email", send_email=False, print_html=None, print_format=None, attachments='[]', send_me_a_copy=False, cc=None, bcc=None, flags=None, read_receipt=None, print_letterhead=True, email_template=None, communication_type=None, - ignore_permissions=False): + ignore_permissions=False) -> Dict[str, str]: """Make a new communication. :param doctype: Reference DocType. @@ -56,7 +59,7 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = cc = list_to_str(cc) if isinstance(cc, list) else cc bcc = list_to_str(bcc) if isinstance(bcc, list) else bcc - comm = frappe.get_doc({ + comm: "Communication" = frappe.get_doc({ "doctype":"Communication", "subject": subject, "content": content, @@ -73,16 +76,13 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "message_id":get_message_id().strip(" <>"), "read_receipt":read_receipt, "has_attachment": 1 if attachments else 0, - "communication_type": communication_type + "communication_type": communication_type, }).insert(ignore_permissions=True) - comm.save(ignore_permissions=True) - - if isinstance(attachments, str): - attachments = json.loads(attachments) - # if not committed, delayed task doesn't find the communication if attachments: + if isinstance(attachments, str): + attachments = json.loads(attachments) add_attachments(comm.name, attachments) if cint(send_email): @@ -93,12 +93,13 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = send_me_a_copy=send_me_a_copy, print_letterhead=print_letterhead) emails_not_sent_to = comm.exclude_emails_list(include_sender=send_me_a_copy) + return { "name": comm.name, - "emails_not_sent_to": ", ".join(emails_not_sent_to or []) + "emails_not_sent_to": ", ".join(emails_not_sent_to) } -def validate_email(doc): +def validate_email(doc: "Communication") -> None: """Validate Email Addresses of Recipients and CC""" if not (doc.communication_type=="Communication" and doc.communication_medium == "Email") or doc.flags.in_receive: return @@ -114,8 +115,6 @@ def validate_email(doc): for email in split_emails(doc.bcc): validate_email_address(email, throw=True) - # validate sender - def set_incoming_outgoing_accounts(doc): from frappe.email.doctype.email_account.email_account import EmailAccount incoming_email_account = EmailAccount.find_incoming( diff --git a/frappe/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py index b6d8070d00..dd9f58342e 100644 --- a/frappe/core/doctype/communication/mixins.py +++ b/frappe/core/doctype/communication/mixins.py @@ -1,3 +1,4 @@ +from typing import List import frappe from frappe import _ from frappe.core.utils import get_parent_doc @@ -194,14 +195,18 @@ class CommunicationEmailMixin: return _("Leave this conversation") return '' - def exclude_emails_list(self, is_inbound_mail_communcation=False, include_sender=False): + def exclude_emails_list(self, is_inbound_mail_communcation=False, include_sender=False) -> List: """List of mail id's excluded while sending mail. """ all_ids = self.get_all_email_addresses(exclude_displayname=True) - final_ids = self.mail_recipients(is_inbound_mail_communcation = is_inbound_mail_communcation) + \ - self.mail_bcc(is_inbound_mail_communcation = is_inbound_mail_communcation) + \ - self.mail_cc(is_inbound_mail_communcation = is_inbound_mail_communcation, include_sender=include_sender) - return set(all_ids) - set(final_ids) + + final_ids = ( + self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation) + + self.mail_bcc(is_inbound_mail_communcation=is_inbound_mail_communcation) + + self.mail_cc(is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=include_sender) + ) + + return list(set(all_ids) - set(final_ids)) def get_assignees(self): """Get owners of the reference document. diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index adf10b9a03..d9c966cc95 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -572,12 +572,10 @@ class File(Document): @staticmethod def zip_files(files): - from six import string_types - zip_file = io.BytesIO() zf = zipfile.ZipFile(zip_file, "w", zipfile.ZIP_DEFLATED) for _file in files: - if isinstance(_file, string_types): + if isinstance(_file, str): _file = frappe.get_doc("File", _file) if not isinstance(_file, File): continue 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_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 488c468025..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 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/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 325b97081b..b5971e236e 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -246,9 +246,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/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') - 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]) + filters = { + 'public': public, + 'label': title + } - doc.content = blocks - doc.save(ignore_permissions=True) + 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]) - if loads(new_widgets): - save_new_widget(doc, title, blocks, new_widgets) + doc.content = blocks + doc.save(ignore_permissions=True) - if loads(sb_public_items) or loads(sb_private_items): - sort_pages(loads(sb_public_items), loads(sb_private_items)) - - if loads(deleted_pages): - return delete_pages(loads(deleted_pages)) + save_new_widget(doc, title, blocks, new_widgets) return {"name": title, "public": public, "label": doc.label} -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")} +@frappe.whitelist() +def update_page(name, title, icon, parent, public): + public = frappe.parse_json(public) - if frappe.db.exists("Workspace", page.get("name")): - frappe.get_doc("Workspace", page.get("name")).delete(ignore_permissions=True) + doc = frappe.get_doc("Workspace", name) - return {"name": "Home", "public": 1, "label": "Home"} + 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.parent_page = parent + 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 name != doc.label: + rename_doc("Workspace", name, doc.label, force=True, ignore_permissions=True) + + # 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) + + return {"name": doc.title, "public": doc.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) + + return doc + +@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": 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/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/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 3f8d399c52..a05e20da24 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -1,5 +1,6 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # License: MIT. See LICENSE + import email.utils import functools import imaplib @@ -7,6 +8,7 @@ import socket import time from datetime import datetime, timedelta from poplib import error_proto +from typing import List import frappe from frappe import _, are_emails_muted, safe_encode @@ -82,9 +84,6 @@ class EmailAccount(Document): if frappe.local.flags.in_patch or frappe.local.flags.in_test: return - #if self.enable_incoming and not self.append_to: - # frappe.throw(_("Append To is mandatory for incoming mails")) - if (not self.awaiting_password and not frappe.local.flags.in_install and not frappe.local.flags.in_patch): if self.password or self.smtp_server in ('127.0.0.1', 'localhost'): @@ -442,7 +441,7 @@ class EmailAccount(Document): frappe.db.rollback() except Exception: frappe.db.rollback() - frappe.log_error('email_account.receive') + frappe.log_error(title="EmailAccount.receive") if self.use_imap: self.handle_bad_emails(mail.uid, mail.raw_message, frappe.get_traceback()) exceptions.append(frappe.get_traceback()) @@ -458,7 +457,7 @@ class EmailAccount(Document): if exceptions: raise Exception(frappe.as_json(exceptions)) - def get_inbound_mails(self, test_mails=None): + def get_inbound_mails(self, test_mails=None) -> List[InboundMail]: """retrive and return inbound mails. """ @@ -625,7 +624,6 @@ class EmailAccount(Document): if frappe.db.exists("Email Account", {"enable_automatic_linking": 1, "name": ('!=', self.name)}): frappe.throw(_("Automatic Linking can be activated only for one Email Account.")) - def append_email_to_sent_folder(self, message): email_server = None try: @@ -643,7 +641,8 @@ class EmailAccount(Document): message = safe_encode(message) email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message) except Exception: - frappe.log_error() + frappe.log_error(title="EmailAccount.append_email_to_sent_folder") + @frappe.whitelist() def get_append_to(doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None): diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py index 9730004065..4da83bd0d2 100644 --- a/frappe/email/doctype/email_queue/email_queue.py +++ b/frappe/email/doctype/email_queue/email_queue.py @@ -11,7 +11,6 @@ import quopri from email.parser import Parser from email.policy import SMTPUTF8 from html2text import html2text -from six.moves import html_parser as HTMLParser import frappe from frappe import _, safe_encode, task @@ -20,6 +19,7 @@ from frappe.email.queue import get_unsubcribed_url, get_unsubscribe_message from frappe.email.email_body import add_attachment, get_formatted_html, get_email from frappe.utils import cint, split_emails, add_days, nowdate, cstr, get_hook_method from frappe.email.doctype.email_account.email_account import EmailAccount +from frappe.query_builder.utils import DocType MAX_RETRY_COUNT = 3 @@ -444,7 +444,7 @@ class QueueBuilder: try: text_content = html2text(self._message) - except HTMLParser.HTMLParseError: + except Exception: text_content = "See html attachment" return text_content + unsubscribe_text_message @@ -477,18 +477,24 @@ class QueueBuilder: all_ids = list(set(self.recipients + self.cc)) - EmailUnsubscribe = frappe.qb.DocType("Email Unsubscribe") + EmailUnsubscribe = DocType("Email Unsubscribe") + + unsubscribed = ( + frappe.qb.from_(EmailUnsubscribe).select( + EmailUnsubscribe.email + ).where( + EmailUnsubscribe.email.isin(all_ids) + & ( + ( + (EmailUnsubscribe.reference_doctype == self.reference_doctype) + & (EmailUnsubscribe.reference_name == self.reference_name) + ) | ( + EmailUnsubscribe.global_unsubscribe == 1 + ) + ) + ).distinct() + ).run(pluck=True) - unsubscribed = (frappe.qb.from_(EmailUnsubscribe) - .select(EmailUnsubscribe.email) - .where(EmailUnsubscribe.email.isin(all_ids) & - ( - ( - (EmailUnsubscribe.reference_doctype == self.reference_doctype) & (EmailUnsubscribe.reference_name == self.reference_name) - ) | EmailUnsubscribe.global_unsubscribe == 1 - ) - ).distinct() - ).run(pluck=True) self._unsubscribed_user_emails = unsubscribed or [] return self._unsubscribed_user_emails 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/document.py b/frappe/model/document.py index db4b7703ba..1217b45aaf 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -474,7 +474,7 @@ class Document(BaseDocument): # We'd probably want the creation and owner to be set via API # or Data import at some point, that'd have to be handled here - if self.is_new(): + if self.is_new() and not (frappe.flags.in_patch or frappe.flags.in_migrate): self.creation = self.modified self.owner = self.modified_by 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 +""" 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 + ``` + + 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 + + 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. + +3. simple python commands can be added by starting line with `execute:` +`execute:` example: `execute:print("hello world")` """ - Execute Patch Files - To run directly +import configparser +import time +from enum import Enum +from typing import List, Optional - python lib/wnf.py patch patch1, patch2 etc - python lib/wnf.py patch -f patch1, patch2 etc +import frappe - where patch1, patch2 is module name -""" -import frappe, frappe.permissions, time -class PatchError(Exception): pass +class PatchError(Exception): + pass -def run_all(skip_failing=False): + +class PatchType(Enum): + pre_model_sync = "pre_model_sync" + post_model_sync = "post_model_sync" + + +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') - for patch in get_all_patches(): + patches = get_all_patches(patch_type=patch_type) + + 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 16ae349941..f2715a23c5 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,7 +123,7 @@ 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', force=True) +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 @@ -175,6 +175,7 @@ execute:frappe.delete_doc_if_exists('Page', 'workspace') execute:frappe.delete_doc_if_exists('Page', 'dashboard', force=1) frappe.core.doctype.page.patches.drop_unused_pages execute:frappe.get_doc('Role', 'Guest').save() # remove desk access +frappe.patches.v13_0.remove_chat frappe.patches.v13_0.rename_desk_page_to_workspace # 02.02.2021 frappe.patches.v13_0.delete_package_publish_tool frappe.patches.v13_0.rename_list_view_setting_to_list_view_settings @@ -184,12 +185,14 @@ frappe.patches.v13_0.queryreport_columns 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/v13_0/remove_chat.py b/frappe/patches/v13_0/remove_chat.py new file mode 100644 index 0000000000..d884419cac --- /dev/null +++ b/frappe/patches/v13_0/remove_chat.py @@ -0,0 +1,16 @@ +import frappe +import click + +def execute(): + frappe.delete_doc_if_exists("DocType", "Chat Message") + frappe.delete_doc_if_exists("DocType", "Chat Profile") + frappe.delete_doc_if_exists("DocType", "Chat Token") + frappe.delete_doc_if_exists("DocType", "Chat Room User") + frappe.delete_doc_if_exists("DocType", "Chat Room") + frappe.delete_doc_if_exists("Module Def", "Chat") + + click.secho( + "Chat Module is moved to a separate app and is removed from Frappe in version-13.\n" + "Please install the app to continue using the chat feature: https://github.com/frappe/chat", + fg="yellow", + ) \ No newline at end of file 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 59ca9c2cb2..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 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,128 +1,309 @@ 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/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/form.js b/frappe/public/js/frappe/form/form.js index 510426a444..5c0b6b1399 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -1149,8 +1149,7 @@ frappe.ui.form.Form = class FrappeForm { subject: __(this.meta.name) + ': ' + this.docname, recipients: this.doc.email || this.doc.email_id || this.doc.contact_email, attach_document_print: true, - message: message, - real_name: this.doc.real_name || this.doc.contact_display || this.doc.contact_name + message: message }); } diff --git a/frappe/public/js/frappe/form/templates/form_sidebar.html b/frappe/public/js/frappe/form/templates/form_sidebar.html index efa8b9ea5e..dcea2f4647 100644 --- a/frappe/public/js/frappe/form/templates/form_sidebar.html +++ b/frappe/public/js/frappe/form/templates/form_sidebar.html @@ -1,5 +1,5 @@ -