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/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/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 6b827a4e89..de28dad900 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -245,9 +245,16 @@ class MariaDBDatabase(Database):
column_name as 'name',
column_type as 'type',
column_default as 'default',
- column_key = 'MUL' as 'index',
+ COALESCE(
+ (select 1
+ from information_schema.statistics
+ where table_name="{table_name}"
+ and column_name=columns.column_name
+ and NON_UNIQUE=1
+ limit 1
+ ), 0) as 'index',
column_key = 'UNI' as 'unique'
- from information_schema.columns
+ from information_schema.columns as columns
where table_name = '{table_name}' '''.format(table_name=table_name), as_dict=1)
def has_index(self, table_name, index_name):
diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py
index 5768a2f23d..07bb4d5d7c 100644
--- a/frappe/database/mariadb/schema.py
+++ b/frappe/database/mariadb/schema.py
@@ -58,18 +58,34 @@ class MariaDBTable(DBTable):
modify_column_query.append("MODIFY `{}` {}".format(col.fieldname, col.get_definition()))
for col in self.add_index:
- # if index key not exists
- if not frappe.db.sql("SHOW INDEX FROM `%s` WHERE key_name = %s" %
- (self.table_name, '%s'), col.fieldname):
- add_index_query.append("ADD INDEX `{}`(`{}`)".format(col.fieldname, col.fieldname))
+ # if index key does not exists
+ if not frappe.db.has_index(self.table_name, col.fieldname + '_index'):
+ add_index_query.append("ADD INDEX `{}_index`(`{}`)".format(col.fieldname, col.fieldname))
- for col in self.drop_index:
+ for col in self.drop_index + self.drop_unique:
if col.fieldname != 'name': # primary key
+ current_column = self.current_columns.get(col.fieldname.lower())
+ unique_constraint_changed = current_column.unique != col.unique
+ if unique_constraint_changed and not col.unique:
+ # nosemgrep
+ unique_index_record = frappe.db.sql("""
+ SHOW INDEX FROM `{0}`
+ WHERE Key_name=%s
+ AND Non_unique=0
+ """.format(self.table_name), (col.fieldname), as_dict=1)
+ if unique_index_record:
+ drop_index_query.append("DROP INDEX `{}`".format(unique_index_record[0].Key_name))
+ index_constraint_changed = current_column.index != col.set_index
# if index key exists
- if frappe.db.sql("""SHOW INDEX FROM `{0}`
- WHERE key_name=%s
- AND Non_unique=%s""".format(self.table_name), (col.fieldname, col.unique)):
- drop_index_query.append("drop index `{}`".format(col.fieldname))
+ if index_constraint_changed and not col.set_index:
+ # nosemgrep
+ index_record = frappe.db.sql("""
+ SHOW INDEX FROM `{0}`
+ WHERE Key_name=%s
+ AND Non_unique=1
+ """.format(self.table_name), (col.fieldname + '_index'), as_dict=1)
+ if index_record:
+ drop_index_query.append("DROP INDEX `{}`".format(index_record[0].Key_name))
try:
for query_parts in [add_column_query, modify_column_query, add_index_query, drop_index_query]:
diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py
index 33f07990af..d5495c6879 100644
--- a/frappe/database/postgres/database.py
+++ b/frappe/database/postgres/database.py
@@ -77,11 +77,11 @@ class PostgresDatabase(Database):
"""Escape quotes and percent in given string."""
if isinstance(s, bytes):
s = s.decode('utf-8')
-
+
# MariaDB's driver treats None as an empty string
# So Postgres should do the same
- if s is None:
+ if s is None:
s = ''
if percent:
@@ -308,18 +308,20 @@ class PostgresDatabase(Database):
WHEN 'timestamp without time zone' THEN 'timestamp'
ELSE a.data_type
END AS type,
- COUNT(b.indexdef) AS Index,
+ BOOL_OR(b.index) AS index,
SPLIT_PART(COALESCE(a.column_default, NULL), '::', 1) AS default,
BOOL_OR(b.unique) AS unique
FROM information_schema.columns a
LEFT JOIN
- (SELECT indexdef, tablename, indexdef LIKE '%UNIQUE INDEX%' AS unique
+ (SELECT indexdef, tablename,
+ indexdef LIKE '%UNIQUE INDEX%' AS unique,
+ indexdef NOT LIKE '%UNIQUE INDEX%' AS index
FROM pg_indexes
WHERE tablename='{table_name}') b
- ON SUBSTRING(b.indexdef, '\(.*\)') LIKE CONCAT('%', a.column_name, '%')
+ ON SUBSTRING(b.indexdef, '(.*)') LIKE CONCAT('%', a.column_name, '%')
WHERE a.table_name = '{table_name}'
- GROUP BY a.column_name, a.data_type, a.column_default, a.character_maximum_length;'''
- .format(table_name=table_name), as_dict=1)
+ GROUP BY a.column_name, a.data_type, a.column_default, a.character_maximum_length;
+ '''.format(table_name=table_name), as_dict=1)
def get_database_list(self, target):
return [d[0] for d in self.sql("SELECT datname FROM pg_database;")]
diff --git a/frappe/database/postgres/schema.py b/frappe/database/postgres/schema.py
index 58153ca6ce..a2d5be0b70 100644
--- a/frappe/database/postgres/schema.py
+++ b/frappe/database/postgres/schema.py
@@ -11,8 +11,6 @@ class PostgresTable(DBTable):
column_defs = self.get_column_definitions()
if column_defs: add_text += ',\n'.join(column_defs)
- # index
- # index_defs = self.get_index_definitions()
# TODO: set docstatus length
# create table
frappe.db.sql("""create table `%s` (
@@ -28,8 +26,25 @@ class PostgresTable(DBTable):
idx bigint not null default '0',
%s)""".format(varchar_len=frappe.db.VARCHAR_LEN) % (self.table_name, add_text))
+ self.create_indexes()
frappe.db.commit()
+ def create_indexes(self):
+ create_index_query = ""
+ for key, col in self.columns.items():
+ if (col.set_index
+ and col.fieldtype in frappe.db.type_map
+ and frappe.db.type_map.get(col.fieldtype)[0]
+ not in ('text', 'longtext')):
+ create_index_query += 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format(
+ index_name=col.fieldname,
+ table_name=self.table_name,
+ field=col.fieldname
+ )
+ if create_index_query:
+ # nosemgrep
+ frappe.db.sql(create_index_query)
+
def alter(self):
for col in self.columns.values():
col.build_for_alter_table(self.current_columns.get(col.fieldname.lower()))
@@ -52,8 +67,8 @@ class PostgresTable(DBTable):
query.append("ALTER COLUMN `{0}` TYPE {1} {2}".format(
col.fieldname,
get_definition(col.fieldtype, precision=col.precision, length=col.length),
- using_clause)
- )
+ using_clause
+ ))
for col in self.set_default:
if col.fieldname=="name":
@@ -73,37 +88,54 @@ class PostgresTable(DBTable):
query.append("ALTER COLUMN `{}` SET DEFAULT {}".format(col.fieldname, col_default))
- create_index_query = ""
+ create_contraint_query = ""
for col in self.add_index:
# if index key not exists
- create_index_query += 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format(
+ create_contraint_query += 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format(
index_name=col.fieldname,
table_name=self.table_name,
field=col.fieldname)
- drop_index_query = ""
+ for col in self.add_unique:
+ # if index key not exists
+ create_contraint_query += 'CREATE UNIQUE INDEX IF NOT EXISTS "unique_{index_name}" ON `{table_name}`(`{field}`);'.format(
+ index_name=col.fieldname,
+ table_name=self.table_name,
+ field=col.fieldname
+ )
+
+ drop_contraint_query = ""
for col in self.drop_index:
# primary key
if col.fieldname != 'name':
# if index key exists
- if not frappe.db.has_index(self.table_name, col.fieldname):
- drop_index_query += 'DROP INDEX IF EXISTS "{}" ;'.format(col.fieldname)
+ drop_contraint_query += 'DROP INDEX IF EXISTS "{}" ;'.format(col.fieldname)
- if query:
- try:
+ for col in self.drop_unique:
+ # primary key
+ if col.fieldname != 'name':
+ # if index key exists
+ drop_contraint_query += 'DROP INDEX IF EXISTS "unique_{}" ;'.format(col.fieldname)
+ try:
+ if query:
final_alter_query = "ALTER TABLE `{}` {}".format(self.table_name, ", ".join(query))
- if final_alter_query: frappe.db.sql(final_alter_query)
- if create_index_query: frappe.db.sql(create_index_query)
- if drop_index_query: frappe.db.sql(drop_index_query)
- except Exception as e:
- # sanitize
- if frappe.db.is_duplicate_fieldname(e):
- frappe.throw(str(e))
- elif frappe.db.is_duplicate_entry(e):
- fieldname = str(e).split("'")[-2]
- frappe.throw(_("""{0} field cannot be set as unique in {1},
- as there are non-unique existing values""".format(
- fieldname, self.table_name)))
- raise e
- else:
- raise e
+ # nosemgrep
+ frappe.db.sql(final_alter_query)
+ if create_contraint_query:
+ # nosemgrep
+ frappe.db.sql(create_contraint_query)
+ if drop_contraint_query:
+ # nosemgrep
+ frappe.db.sql(drop_contraint_query)
+ except Exception as e:
+ # sanitize
+ if frappe.db.is_duplicate_fieldname(e):
+ frappe.throw(str(e))
+ elif frappe.db.is_duplicate_entry(e):
+ fieldname = str(e).split("'")[-2]
+ frappe.throw(
+ _("{0} field cannot be set as unique in {1}, as there are non-unique existing values")
+ .format(fieldname, self.table_name)
+ )
+ else:
+ raise e
diff --git a/frappe/database/query.py b/frappe/database/query.py
index 6d2be5fa25..587378b32f 100644
--- a/frappe/database/query.py
+++ b/frappe/database/query.py
@@ -308,7 +308,7 @@ class Permission:
doctype = [doctype]
for dt in doctype:
- dt = re.sub("tab", "", dt)
+ dt = re.sub("^tab", "", dt)
if not frappe.has_permission(
dt,
"select",
diff --git a/frappe/database/schema.py b/frappe/database/schema.py
index 10582eff8f..9a6dd502dc 100644
--- a/frappe/database/schema.py
+++ b/frappe/database/schema.py
@@ -21,6 +21,7 @@ class DBTable:
self.change_name = []
self.add_unique = []
self.add_index = []
+ self.drop_unique = []
self.drop_index = []
self.set_default = []
@@ -219,8 +220,10 @@ class DbColumn:
self.table.change_type.append(self)
# unique
- if((self.unique and not current_def['unique']) and column_type not in ('text', 'longtext')):
+ if ((self.unique and not current_def['unique']) and column_type not in ('text', 'longtext')):
self.table.add_unique.append(self)
+ elif (current_def['unique'] and not self.unique) and column_type not in ('text', 'longtext'):
+ self.table.drop_unique.append(self)
# default
if (self.default_changed(current_def)
@@ -230,9 +233,7 @@ class DbColumn:
self.table.set_default.append(self)
# index should be applied or dropped irrespective of type change
- if ((current_def['index'] and not self.set_index and not self.unique)
- or (current_def['unique'] and not self.unique)):
- # to drop unique you have to drop index
+ if (current_def['index'] and not self.set_index) and column_type not in ('text', 'longtext'):
self.table.drop_index.append(self)
elif (not current_def['index'] and self.set_index) and not (column_type in ('text', 'longtext')):
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index e1789852f1..4164db679d 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -56,31 +56,6 @@ class Workspace:
self.restricted_doctypes = frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache()
self.restricted_pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache()
- def is_page_allowed(self):
- cards = self.doc.get_link_groups() + get_custom_reports_and_doctypes(self.doc.module)
- shortcuts = self.doc.shortcuts
-
- for section in cards:
- links = loads(section.get('links')) if isinstance(section.get('links'), str) else section.get('links')
- for item in links:
- if self.is_item_allowed(item.get('link_to'), item.get('link_type')):
- return True
-
- def _in_active_domains(item):
- if not item.restrict_to_domain:
- return True
- else:
- return item.restrict_to_domain in frappe.get_active_domains()
-
- for item in shortcuts:
- if self.is_item_allowed(item.link_to, item.type) and _in_active_domains(item):
- return True
-
- if not shortcuts and not self.doc.links:
- return True
-
- return False
-
def is_permitted(self):
"""Returns true if Has Role is not set or the user is allowed."""
from frappe.utils import has_common
@@ -346,20 +321,20 @@ def get_desktop_page(page):
dict: dictionary of cards, charts and shortcuts to be displayed on website
"""
try:
- wspace = Workspace(loads(page))
- wspace.build_workspace()
+ workspace = Workspace(loads(page))
+ workspace.build_workspace()
return {
- 'charts': wspace.charts,
- 'shortcuts': wspace.shortcuts,
- 'cards': wspace.cards,
- 'onboardings': wspace.onboardings
+ 'charts': workspace.charts,
+ 'shortcuts': workspace.shortcuts,
+ 'cards': workspace.cards,
+ 'onboardings': workspace.onboardings
}
except DoesNotExistError:
frappe.log_error(frappe.get_traceback())
return {}
@frappe.whitelist()
-def get_wspace_sidebar_items():
+def get_workspace_sidebar_items():
"""Get list of sidebar items for desk"""
has_access = "Workspace Manager" in frappe.get_roles()
@@ -385,8 +360,8 @@ def get_wspace_sidebar_items():
# Filter Page based on Permission
for page in all_pages:
try:
- wspace = Workspace(page, True)
- if wspace.is_permitted() and wspace.is_page_allowed() or has_access:
+ workspace = Workspace(page, True)
+ if has_access or workspace.is_permitted():
if page.public:
pages.append(page)
elif page.for_user == frappe.session.user:
@@ -453,25 +428,24 @@ def get_custom_report_list(module):
return out
def save_new_widget(doc, page, blocks, new_widgets):
+ if loads(new_widgets):
+ widgets = _dict(loads(new_widgets))
- widgets = _dict(loads(new_widgets))
-
- if widgets.chart:
- doc.charts.extend(new_widget(widgets.chart, "Workspace Chart", "charts"))
- if widgets.shortcut:
- doc.shortcuts.extend(new_widget(widgets.shortcut, "Workspace Shortcut", "shortcuts"))
- if widgets.card:
- doc.build_links_table_from_card(widgets.card)
+ if widgets.chart:
+ doc.charts.extend(new_widget(widgets.chart, "Workspace Chart", "charts"))
+ if widgets.shortcut:
+ doc.shortcuts.extend(new_widget(widgets.shortcut, "Workspace Shortcut", "shortcuts"))
+ if widgets.card:
+ doc.build_links_table_from_card(widgets.card)
# remove duplicate and unwanted widgets
- if widgets:
- clean_up(doc, blocks)
+ clean_up(doc, blocks)
try:
doc.save(ignore_permissions=True)
except (ValidationError, TypeError) as e:
# Create a json string to log
- json_config = dumps(widgets, sort_keys=True, indent=4)
+ json_config = widgets and dumps(widgets, sort_keys=True, indent=4)
# Error log body
log = \
diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json
index 04975c69e3..211029dfcf 100644
--- a/frappe/desk/doctype/workspace/workspace.json
+++ b/frappe/desk/doctype/workspace/workspace.json
@@ -1,5 +1,6 @@
{
"actions": [],
+ "allow_rename": 1,
"autoname": "field:label",
"beta": 1,
"creation": "2020-01-23 13:45:59.470592",
@@ -141,7 +142,7 @@
},
{
"fieldname": "sequence_id",
- "fieldtype": "Int",
+ "fieldtype": "Float",
"label": "Sequence Id"
},
{
@@ -158,7 +159,7 @@
],
"in_create": 1,
"links": [],
- "modified": "2021-09-16 12:01:06.450622",
+ "modified": "2021-12-15 19:33:00.805265",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace",
diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py
index 94114e3918..b40f517350 100644
--- a/frappe/desk/doctype/workspace/workspace.py
+++ b/frappe/desk/doctype/workspace/workspace.py
@@ -6,6 +6,7 @@ import frappe
from frappe import _
from frappe.modules.export_file import export_to_files
from frappe.model.document import Document
+from frappe.model.rename_doc import rename_doc
from frappe.desk.desktop import save_new_widget
from frappe.desk.utils import validate_route_conflict
@@ -121,77 +122,157 @@ def get_report_type(report):
report_type = frappe.get_value("Report", report, "report_type")
return report_type in ["Query Report", "Script Report", "Custom Report"]
+@frappe.whitelist()
+def new_page(new_page):
+ if not loads(new_page):
+ return
+
+ page = loads(new_page)
+
+ if page.get("public") and not is_workspace_manager():
+ return
+
+ doc = frappe.new_doc('Workspace')
+ doc.title = page.get('title')
+ doc.icon = page.get('icon')
+ doc.content = page.get('content')
+ doc.parent_page = page.get('parent_page')
+ doc.label = page.get('label')
+ doc.for_user = page.get('for_user')
+ doc.public = page.get('public')
+ doc.sequence_id = last_sequence_id(doc) + 1
+ doc.save(ignore_permissions=True)
+
+ return doc
@frappe.whitelist()
-def save_page(title, icon, parent, public, sb_public_items, sb_private_items, deleted_pages, new_widgets, blocks, save):
- save = frappe.parse_json(save)
+def save_page(title, public, new_widgets, blocks):
public = frappe.parse_json(public)
- if save:
- doc = frappe.new_doc('Workspace')
+
+ filters = {
+ 'public': public,
+ 'label': title
+ }
+
+ if not public:
+ filters = {
+ 'for_user': frappe.session.user,
+ 'label': title + "-" + frappe.session.user
+ }
+ pages = frappe.get_list("Workspace", filters=filters)
+ if pages:
+ doc = frappe.get_doc("Workspace", pages[0])
+
+ doc.content = blocks
+ doc.save(ignore_permissions=True)
+
+ save_new_widget(doc, title, blocks, new_widgets)
+
+ return {"name": title, "public": public, "label": doc.label}
+
+@frappe.whitelist()
+def update_page(name, title, icon, parent, public):
+ public = frappe.parse_json(public)
+
+ doc = frappe.get_doc("Workspace", name)
+
+ filters = {
+ 'parent_page': doc.title,
+ 'public': doc.public
+ }
+ child_docs = frappe.get_list("Workspace", filters=filters)
+
+ if doc:
doc.title = title
doc.icon = icon
- doc.content = blocks
doc.parent_page = parent
-
- if public:
- doc.label = title
- doc.public = 1
- else:
- doc.label = title + "-" + frappe.session.user
- doc.for_user = frappe.session.user
- doc.save(ignore_permissions=True)
- else:
- if public:
- filters = {
- 'public': public,
- 'label': title
- }
- else:
- filters = {
- 'for_user': frappe.session.user,
- 'label': title + "-" + frappe.session.user
- }
- pages = frappe.get_list("Workspace", filters=filters)
- if pages:
- doc = frappe.get_doc("Workspace", pages[0])
-
- doc.content = blocks
+ if doc.public != public:
+ doc.sequence_id = frappe.db.count('Workspace', {'public':public}, cache=True)
+ doc.public = public
+ doc.for_user = '' if public else doc.for_user or frappe.session.user
+ doc.label = '{0}-{1}'.format(title, doc.for_user) if doc.for_user else title
doc.save(ignore_permissions=True)
- if loads(new_widgets):
- save_new_widget(doc, title, blocks, new_widgets)
+ if name != doc.label:
+ rename_doc("Workspace", name, doc.label, force=True, ignore_permissions=True)
- if loads(sb_public_items) or loads(sb_private_items):
- sort_pages(loads(sb_public_items), loads(sb_private_items))
+ # update new name and public in child pages
+ if child_docs:
+ for child in child_docs:
+ child_doc = frappe.get_doc("Workspace", child.name)
+ child_doc.parent_page = doc.title
+ child_doc.public = doc.public
+ child_doc.save(ignore_permissions=True)
- if loads(deleted_pages):
- return delete_pages(loads(deleted_pages))
+ return {"name": doc.title, "public": doc.public, "label": doc.label}
- return {"name": title, "public": public, "label": doc.label}
+@frappe.whitelist()
+def duplicate_page(page_name, new_page):
+ if not loads(new_page):
+ return
+
+ new_page = loads(new_page)
+
+ if new_page.get("is_public") and not is_workspace_manager():
+ return
+
+ old_doc = frappe.get_doc("Workspace", page_name)
+ doc = frappe.copy_doc(old_doc)
+ doc.title = new_page.get('title')
+ doc.icon = new_page.get('icon')
+ doc.parent_page = new_page.get('parent') or ''
+ doc.public = new_page.get('is_public')
+ doc.for_user = ''
+ doc.label = doc.title
+ if not doc.public:
+ doc.for_user = doc.for_user or frappe.session.user
+ doc.label = '{0}-{1}'.format(doc.title, doc.for_user)
+ doc.name = doc.label
+ if old_doc.public == doc.public:
+ doc.sequence_id += 0.1
+ else:
+ doc.sequence_id = last_sequence_id(doc) + 1
+ doc.insert(ignore_permissions=True)
-def delete_pages(deleted_pages):
- for page in deleted_pages:
- if page.get("public") and not is_workspace_manager():
- return {"name": page.get("title"), "public": 1, "label": page.get("label")}
+ return doc
- if frappe.db.exists("Workspace", page.get("name")):
- frappe.get_doc("Workspace", page.get("name")).delete(ignore_permissions=True)
+@frappe.whitelist()
+def delete_page(page):
+ if not loads(page):
+ return
+
+ page = loads(page)
+
+ if page.get("public") and not is_workspace_manager():
+ return
+
+ if frappe.db.exists("Workspace", page.get("name")):
+ frappe.get_doc("Workspace", page.get("name")).delete(ignore_permissions=True)
- return {"name": "Home", "public": 1, "label": "Home"}
+ return {"name": page.get("name"), "public": page.get("public"), "title": page.get("title")}
+@frappe.whitelist()
def sort_pages(sb_public_items, sb_private_items):
- wspace_public_pages = get_page_list(['name', 'title'], {'public': 1})
- wspace_private_pages = get_page_list(['name', 'title'], {'for_user': frappe.session.user})
+ if not loads(sb_public_items) and not loads(sb_private_items):
+ return
+
+ sb_public_items = loads(sb_public_items)
+ sb_private_items = loads(sb_private_items)
+
+ workspace_public_pages = get_page_list(['name', 'title'], {'public': 1})
+ workspace_private_pages = get_page_list(['name', 'title'], {'for_user': frappe.session.user})
if sb_private_items:
- sort_page(wspace_private_pages, sb_private_items)
+ return sort_page(workspace_private_pages, sb_private_items)
if sb_public_items and is_workspace_manager():
- sort_page(wspace_public_pages, sb_public_items)
+ return sort_page(workspace_public_pages, sb_public_items)
-def sort_page(wspace_pages, pages):
+ return False
+
+def sort_page(workspace_pages, pages):
for seq, d in enumerate(pages):
- for page in wspace_pages:
+ for page in workspace_pages:
if page.title == d.get('title'):
doc = frappe.get_doc('Workspace', page.name)
doc.sequence_id = seq + 1
@@ -199,6 +280,27 @@ def sort_page(wspace_pages, pages):
doc.save(ignore_permissions=True)
break
+ return True
+
+def last_sequence_id(doc):
+ doc_exists = frappe.db.exists({
+ 'doctype': 'Workspace',
+ 'public': doc.public,
+ 'for_user': doc.for_user
+ })
+
+ if not doc_exists:
+ return 0
+
+ return frappe.db.get_list('Workspace',
+ fields=['sequence_id'],
+ filters={
+ 'public': doc.public,
+ 'for_user': doc.for_user
+ },
+ order_by="sequence_id desc"
+ )[0].sequence_id
+
def get_page_list(fields, filters):
return frappe.get_list("Workspace", fields=fields, filters=filters, order_by='sequence_id asc')
diff --git a/frappe/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/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/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..75d4972152 100644
--- a/frappe/modules/patch_handler.py
+++ b/frappe/modules/patch_handler.py
@@ -1,37 +1,76 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
-"""
- Execute Patch Files
+""" Patch Handler.
+
+This file manages execution of manaully written patches. Patches are script
+that apply changes in database schema or data to accomodate for changes in the
+code.
+
+Ways to specify patches:
+
+1. patches.txt file specifies patches that run before doctype schema
+migration. Each line represents one patch (old format).
+2. patches.txt can alternatively also separate pre and post model sync
+patches by using INI like file format:
+ ```patches.txt
+ [pre_model_sync]
+ app.module.patch1
+ app.module.patch2
- To run directly
- python lib/wnf.py patch patch1, patch2 etc
- python lib/wnf.py patch -f patch1, patch2 etc
+ [post_model_sync]
+ app.module.patch3
+ ```
- where patch1, patch2 is module name
+ 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")`
"""
-import frappe, frappe.permissions, time
-class PatchError(Exception): pass
+import configparser
+import time
+from enum import Enum
+from typing import List, Optional
+
+import frappe
-def run_all(skip_failing=False):
+
+class PatchError(Exception):
+ pass
+
+
+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')
+
+ patches = get_all_patches(patch_type=patch_type)
- for patch in get_all_patches():
+ for patch in patches:
if patch and (patch not in executed):
run_patch(patch)
@@ -40,18 +79,54 @@ 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)
+
+
+ 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 +148,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 +171,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 +184,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 +200,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/patches.txt b/frappe/patches.txt
index 16ae349941..c393b456e3 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -1,3 +1,4 @@
+[pre_model_sync]
frappe.patches.v12_0.remove_deprecated_fields_from_doctype #3
execute:frappe.utils.global_search.setup_global_search_table()
execute:frappe.reload_doc('core', 'doctype', 'doctype_action', force=True) #2019-09-23
@@ -87,7 +88,6 @@ frappe.patches.v11_0.set_missing_creation_and_modified_value_for_user_permission
frappe.patches.v11_0.set_default_letter_head_source
frappe.patches.v12_0.set_primary_key_in_series
execute:frappe.delete_doc("Page", "modules", ignore_missing=True)
-frappe.patches.v11_0.set_default_letter_head_source
frappe.patches.v12_0.setup_comments_from_communications
frappe.patches.v12_0.replace_null_values_in_tables
frappe.patches.v12_0.reset_home_settings
@@ -123,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
@@ -184,12 +184,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/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,217 +1,262 @@
diff --git a/frappe/public/js/desk.bundle.js b/frappe/public/js/desk.bundle.js
index cac02c7a68..e056a34be2 100644
--- a/frappe/public/js/desk.bundle.js
+++ b/frappe/public/js/desk.bundle.js
@@ -96,6 +96,7 @@ import "./frappe/ui/sort_selector.js";
import "./frappe/change_log.html";
import "./frappe/ui/workspace_loading_skeleton.html";
+import "./frappe/ui/workspace_sidebar_loading_skeleton.html";
import "./frappe/desk.js";
import "./frappe/query_string.js";
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index 202cee645a..51ada70948 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -214,19 +214,20 @@ frappe.Application = class Application {
email_password_prompt(email_account,user,i) {
var me = this;
+ const email_id = email_account[i]["email_id"];
let d = new frappe.ui.Dialog({
title: __('Password missing in Email Account'),
fields: [
{
'fieldname': 'password',
'fieldtype': 'Password',
- 'label': __('Please enter the password for: {0}', [email_account[i]["email_id"]]),
+ 'label': __('Please enter the password for: {0}', [email_id], "Email Account"),
'reqd': 1
},
{
"fieldname": "submit",
"fieldtype": "Button",
- "label": __("Submit")
+ "label": __("Submit", null, "Submit password for Email Account")
}
]
});
diff --git a/frappe/public/js/frappe/form/controls/date_range.js b/frappe/public/js/frappe/form/controls/date_range.js
index 727e9d55c2..170404f575 100644
--- a/frappe/public/js/frappe/form/controls/date_range.js
+++ b/frappe/public/js/frappe/form/controls/date_range.js
@@ -11,7 +11,8 @@ frappe.ui.form.ControlDateRange = class ControlDateRange extends frappe.ui.form.
language: "en",
range: true,
autoClose: true,
- toggleSelected: false
+ toggleSelected: false,
+ firstDay: frappe.datetime.get_first_day_of_the_week_index()
};
this.datepicker_options.dateFormat =
(frappe.boot.sysdefaults.date_format || 'yyyy-mm-dd');
diff --git a/frappe/public/js/frappe/form/controls/dynamic_link.js b/frappe/public/js/frappe/form/controls/dynamic_link.js
index 2c5661ca87..ea9ceb35f3 100644
--- a/frappe/public/js/frappe/form/controls/dynamic_link.js
+++ b/frappe/public/js/frappe/form/controls/dynamic_link.js
@@ -2,7 +2,7 @@ frappe.ui.form.ControlDynamicLink = class ControlDynamicLink extends frappe.ui.f
get_options() {
let options = '';
if (this.df.get_options) {
- options = this.df.get_options();
+ options = this.df.get_options(this);
} else if (this.docname==null && cur_dialog) {
//for dialog box
options = cur_dialog.get_value(this.df.options);
diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js
index bbc6ecda28..5c0b6b1399 100644
--- a/frappe/public/js/frappe/form/form.js
+++ b/frappe/public/js/frappe/form/form.js
@@ -943,7 +943,10 @@ frappe.ui.form.Form = class FrappeForm {
// re-enable buttons
resolve();
}
- frappe.throw (__("No permission to '{0}' {1}", [__(action), __(this.doc.doctype)]));
+
+ frappe.throw(
+ __("No permission to '{0}' {1}", [__(action), __(this.doc.doctype)], "{0} = verb, {1} = object")
+ );
}
}
diff --git a/frappe/public/js/frappe/form/save.js b/frappe/public/js/frappe/form/save.js
index 65d84e2202..934c90f017 100644
--- a/frappe/public/js/frappe/form/save.js
+++ b/frappe/public/js/frappe/form/save.js
@@ -7,12 +7,12 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
$(btn).prop("disabled", true);
// specified here because there are keyboard shortcuts to save
- var working_label = {
- "Save": __("Saving"),
- "Submit": __("Submitting"),
- "Update": __("Updating"),
- "Amend": __("Amending"),
- "Cancel": __("Cancelling")
+ const working_label = {
+ "Save": __("Saving", null, "Freeze message while saving a document"),
+ "Submit": __("Submitting", null, "Freeze message while submitting a document"),
+ "Update": __("Updating", null, "Freeze message while updating a document"),
+ "Amend": __("Amending", null, "Freeze message while amending a document"),
+ "Cancel": __("Cancelling", null, "Freeze message while cancelling a document"),
}[toTitle(action)];
var freeze_message = working_label ? __(working_label) : "";
@@ -154,8 +154,8 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
if (error_fields.length) {
let meta = frappe.get_meta(doc.doctype);
if (meta.istable) {
- var message = __('Mandatory fields required in table {0}, Row {1}',
- [__(frappe.meta.docfield_map[doc.parenttype][doc.parentfield].label).bold(), doc.idx]);
+ const table_label = __(frappe.meta.docfield_map[doc.parenttype][doc.parentfield].label).bold();
+ var message = __('Mandatory fields required in table {0}, Row {1}', [table_label, doc.idx]);
} else {
var message = __('Mandatory fields required in {0}', [__(doc.doctype)]);
}
@@ -276,4 +276,3 @@ frappe.ui.form.update_calling_link = (newdoc) => {
frappe._from_link = null;
}
}
-
diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js
index 22f8377a57..3cde04313f 100644
--- a/frappe/public/js/frappe/list/list_view.js
+++ b/frappe/public/js/frappe/list/list_view.js
@@ -200,7 +200,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
frappe.render_template("list_view_permission_restrictions", {
condition_list: match_rules_list,
}),
- __("Restrictions")
+ __("Restrictions", null, "Title of message showing restrictions in list view")
);
}
@@ -255,8 +255,13 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
set_primary_action() {
if (this.can_create) {
+ const doctype_name = __(frappe.router.doctype_layout) || __(this.doctype);
+
+ // Better style would be __("Add {0}", [doctype_name], "Primary action in list view")
+ // Keeping it like this to not disrupt existing translations
+ const label = `${__("Add", null, "Primary action in list view")} ${doctype_name}`;
this.page.set_primary_action(
- `${__("Add")} ${frappe.router.doctype_layout || __(this.doctype)}`,
+ label,
() => {
if (this.settings.primary_action) {
this.settings.primary_action();
@@ -320,9 +325,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
setup_freeze_area() {
this.$freeze = $(
- `
${__(
- "Loading"
- )}...
`
+ `
+ ${__("Loading")}...
+
`
).hide();
this.$result.append(this.$freeze);
}
@@ -460,8 +465,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
? __("No {0} found", [__(this.doctype)])
: __("You haven't created a {0} yet", [__(this.doctype)]);
let new_button_label = filters && filters.length
- ? __("Create a new {0}", [__(this.doctype)])
- : __("Create your first {0}", [__(this.doctype)]);
+ ? __("Create a new {0}", [__(this.doctype)], "Create a new document from list view")
+ : __("Create your first {0}", [__(this.doctype)], "Create a new document from list view");
let empty_state_image =
this.settings.empty_state_image ||
"/assets/frappe/images/ui-states/list-empty-state.svg";
@@ -469,7 +474,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
const new_button = this.can_create
? `