From 0d7024fa3cef712ff35304ba60ead2e6fbeffd20 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 11 Apr 2022 23:32:16 +0200 Subject: [PATCH 01/57] fix: handle no data in set_open_count --- frappe/public/js/frappe/form/dashboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index 6e3dd3eb0b..0731bdf8fb 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -375,7 +375,7 @@ frappe.ui.form.Dashboard = class FormDashboard { } set_open_count() { - if (!this.data.transactions || !this.data.fieldname) { + if (!this.data || (!this.data.transactions || !this.data.fieldname)) { return; } From 839eee7bd53626b549ca1717c13fc502829a332b Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Sat, 16 Apr 2022 08:04:07 +0530 Subject: [PATCH 02/57] fix: current job was not shown in workers view because of `if` conditions that should only be checked when view is Jobs --- .../page/background_jobs/background_jobs.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py index d3f5e3d32f..09213d64c3 100644 --- a/frappe/core/page/background_jobs/background_jobs.py +++ b/frappe/core/page/background_jobs/background_jobs.py @@ -22,11 +22,7 @@ JOB_COLORS = {"queued": "orange", "failed": "red", "started": "blue", "finished" def get_info(view=None, queue_timeout=None, job_status=None) -> List[Dict]: jobs = [] - def add_job(job: "Job", name: str) -> None: - if job_status != "all" and job.get_status() != job_status: - return - if queue_timeout != "all" and not name.endswith(f":{queue_timeout}"): - return + def add_job(job: "Job", queue: str) -> None: if job.kwargs.get("site") == frappe.local.site: job_info = { @@ -34,7 +30,7 @@ def get_info(view=None, queue_timeout=None, job_status=None) -> List[Dict]: or job.kwargs.get("kwargs", {}).get("job_type") or str(job.kwargs.get("job_name")), "status": job.get_status(), - "queue": name, + "queue": queue, "creation": convert_utc_to_user_timezone(job.created_at), "color": JOB_COLORS[job.get_status()], } @@ -48,14 +44,21 @@ def get_info(view=None, queue_timeout=None, job_status=None) -> List[Dict]: queues = get_queues() for queue in queues: for job in queue.jobs: + if job_status != "all" and job.get_status() != job_status: + return + if queue_timeout != "all" and not queue.name.endswith(f":{queue_timeout}"): + return add_job(job, queue.name) elif view == "Workers": workers = get_workers() for worker in workers: current_job = worker.get_current_job() - if current_job and current_job.kwargs.get("site") == frappe.local.site: - add_job(current_job, job.origin) + if current_job: + if hasattr(current_job, "kwargs") and current_job.kwargs.get("site") == frappe.local.site: + add_job(current_job, current_job.origin) + else: + jobs.append({"queue": worker.name, "job_name": "busy", "status": "", "creation": ""}) else: jobs.append({"queue": worker.name, "job_name": "idle", "status": "", "creation": ""}) From bafb1faa4891c9be76c3d12aa4f13cbe48ab4b88 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 18 Apr 2022 10:51:41 +0530 Subject: [PATCH 03/57] chore: Add semantic releases --- .github/workflows/release.yml | 25 +++++++++++++++++++++++++ .releaserc | 25 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 .releaserc diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..d7cf89f017 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,25 @@ +name: Generate Semantic Release +on: + push: + branches: + - version-13 +jobs: + release: + name: Release + runs-on: ubuntu-18.04 + steps: + - name: Checkout Entire Repository + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Setup Node.js v14 + uses: actions/setup-node@v2 + with: + node-version: 14 + - name: Setup dependencies + run: | + npm install @semantic-release/git @semantic-release/exec --no-save + - name: Create Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: npx semantic-release \ No newline at end of file diff --git a/.releaserc b/.releaserc new file mode 100644 index 0000000000..9b18977478 --- /dev/null +++ b/.releaserc @@ -0,0 +1,25 @@ +{ + "branches": ["version-13"], + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + [ + "@semantic-release/exec", { + "prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" frappe/__init__.py' + } + ], + [ + "@semantic-release/git", { + "assets": ["frappe/__init__.py"], + "message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}" + } + ], + [ + "@semantic-release/github", { + "assets": [ + {"path": "dist/*"}, + ] + } + ] + ] +} \ No newline at end of file From 6c7d867dbd4e8dc83f46a958fccc166e5850c681 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 18 Apr 2022 13:10:59 +0530 Subject: [PATCH 04/57] chore: block major releases --- .releaserc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.releaserc b/.releaserc index 9b18977478..2b3f4975db 100644 --- a/.releaserc +++ b/.releaserc @@ -1,7 +1,12 @@ { "branches": ["version-13"], "plugins": [ - "@semantic-release/commit-analyzer", + "@semantic-release/commit-analyzer", { + "preset": "angular", + "releaseRules": [ + {"breaking": true, "release": false} + ] + }, "@semantic-release/release-notes-generator", [ "@semantic-release/exec", { From 803f1fb0619c18942c547fcabe99753d1eb64f54 Mon Sep 17 00:00:00 2001 From: Himanshu Date: Mon, 18 Apr 2022 11:15:19 +0100 Subject: [PATCH 05/57] feat: add/remove fields from kanban board (#16257) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Co-authored-by: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> --- cypress/integration/kanban.js | 84 ++++++ cypress/support/commands.js | 1 + frappe/commands/utils.py | 2 +- .../doctype/kanban_board/kanban_board.json | 261 ++++------------- .../desk/doctype/kanban_board/kanban_board.py | 22 +- frappe/public/js/frappe/list/list_view.js | 17 +- .../js/frappe/views/kanban/kanban_board.js | 33 ++- .../js/frappe/views/kanban/kanban_card.html | 33 +-- .../js/frappe/views/kanban/kanban_settings.js | 264 ++++++++++++++++++ .../js/frappe/views/kanban/kanban_view.js | 22 ++ frappe/public/scss/desk/kanban.scss | 224 ++++++++------- 11 files changed, 614 insertions(+), 349 deletions(-) create mode 100644 cypress/integration/kanban.js create mode 100644 frappe/public/js/frappe/views/kanban/kanban_settings.js diff --git a/cypress/integration/kanban.js b/cypress/integration/kanban.js new file mode 100644 index 0000000000..bb9aaf3d8c --- /dev/null +++ b/cypress/integration/kanban.js @@ -0,0 +1,84 @@ +context('Kanban Board', () => { + before(() => { + cy.login(); + cy.visit('/app'); + }); + + it('Create ToDo Kanban', () => { + cy.visit('/app/todo'); + + cy.get('.page-actions .custom-btn-group button').click(); + cy.get('.page-actions .custom-btn-group ul.dropdown-menu li').contains('Kanban').click(); + + cy.fill_field('board_name', 'ToDo Kanban', 'Data'); + cy.fill_field('field_name', 'Status', 'Select'); + cy.click_modal_primary_button('Save'); + + cy.get('.title-text').should('contain', 'ToDo Kanban'); + }); + + it('Create ToDo from kanban', () => { + cy.intercept({ + method: 'POST', + url: 'api/method/frappe.client.save' + }).as('save-todo'); + + cy.click_listview_primary_button('Add ToDo'); + + cy.fill_field('description', 'Test Kanban ToDo', 'Text Editor'); + cy.get('.modal-footer .btn-primary').last().click(); + + cy.wait('@save-todo'); + }); + + it('Add and Remove fields', () => { + cy.visit('/app/todo/view/kanban/ToDo Kanban'); + + cy.intercept('POST', '/api/method/frappe.desk.doctype.kanban_board.kanban_board.save_settings').as('save-kanban'); + cy.intercept('POST', '/api/method/frappe.desk.doctype.kanban_board.kanban_board.update_order').as('update-order'); + + cy.get('.page-actions .menu-btn-group > .btn').click(); + cy.get('.page-actions .menu-btn-group .dropdown-menu li').contains('Kanban Settings').click(); + cy.get('.add-new-fields').click(); + + cy.get('.checkbox-options .checkbox').contains('ID').click(); + cy.get('.checkbox-options .checkbox').contains('Status').first().click(); + cy.get('.checkbox-options .checkbox').contains('Priority').click(); + + cy.get('.modal-footer .btn-primary').last().click(); + + cy.get('.frappe-control .label-area').contains('Show Labels').click(); + cy.click_modal_primary_button('Save'); + + cy.wait('@save-kanban'); + + cy.get('.kanban-column[data-column-value="Open"] .kanban-cards').as('open-cards'); + cy.get('@open-cards').find('.kanban-card .kanban-card-doc').first().should('contain', 'ID:'); + cy.get('@open-cards').find('.kanban-card .kanban-card-doc').first().should('contain', 'Status:'); + cy.get('@open-cards').find('.kanban-card .kanban-card-doc').first().should('contain', 'Priority:'); + + cy.get('.page-actions .menu-btn-group > .btn').click(); + cy.get('.page-actions .menu-btn-group .dropdown-menu li').contains('Kanban Settings').click(); + cy.get_open_dialog().find('.frappe-control[data-fieldname="fields_html"] div[data-label="ID"] .remove-field').click(); + + cy.wait('@update-order'); + cy.get_open_dialog().find('.frappe-control .label-area').contains('Show Labels').click(); + cy.get('.modal-footer .btn-primary').last().click(); + + cy.wait('@save-kanban'); + + cy.get('@open-cards').find('.kanban-card .kanban-card-doc').first().should('not.contain', 'ID:'); + + }); + + it('Drag todo', () => { + cy.intercept({ + method: 'POST', + url: 'api/method/frappe.desk.doctype.kanban_board.kanban_board.update_order_for_single_card' + }).as('drag-completed'); + + cy.get('.kanban-card-body:first').drag('[data-column-value="Closed"] .kanban-cards', {force: true}); + + cy.wait('@drag-completed'); + }); +}); \ No newline at end of file diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 636312376d..4847dbcf12 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -1,5 +1,6 @@ import 'cypress-file-upload'; import '@testing-library/cypress/add-commands'; +import '@4tw/cypress-drag-drop'; // *********************************************** // This example commands.js shows you how to // create various custom commands and overwrite diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 2e27b8d6fe..bb1a8bc2d2 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -870,7 +870,7 @@ def run_ui_tests( # install cypress click.secho("Installing Cypress...", fg="yellow") frappe.commands.popen( - "yarn add cypress@^6 cypress-file-upload@^5 @testing-library/cypress@^8 @cypress/code-coverage@^3 --no-lockfile" + "yarn add cypress@^6 cypress-file-upload@^5 @4tw/cypress-drag-drop@^2 @testing-library/cypress@^8 @cypress/code-coverage@^3 --no-lockfile" ) # run for headless mode diff --git a/frappe/desk/doctype/kanban_board/kanban_board.json b/frappe/desk/doctype/kanban_board/kanban_board.json index f2e1a78d40..b1f120687c 100644 --- a/frappe/desk/doctype/kanban_board/kanban_board.json +++ b/frappe/desk/doctype/kanban_board/kanban_board.json @@ -1,267 +1,124 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, + "actions": [], "allow_rename": 1, "autoname": "field:kanban_board_name", - "beta": 0, "creation": "2016-10-19 12:26:04.809812", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "kanban_board_name", + "reference_doctype", + "field_name", + "column_break_4", + "private", + "show_labels", + "section_break_3", + "columns", + "filters", + "fields" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "kanban_board_name", "fieldtype": "Data", - "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": "Kanban Board Name", - "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": 1 }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "reference_doctype", "fieldtype": "Link", - "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": "Reference Document Type", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "reqd": 1 }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "field_name", "fieldtype": "Select", - "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": "Field Name", - "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 + "reqd": 1 }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_3", - "fieldtype": "Section Break", - "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, - "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": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "columns", "fieldtype": "Table", - "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": "Columns", - "length": 0, - "no_copy": 0, - "options": "Kanban Board Column", - "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 + "options": "Kanban Board Column" }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "filters", - "fieldtype": "Text", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, + "fieldtype": "Code", "label": "Filters", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "options": "JSON", + "read_only": 1 }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fieldname": "private", "fieldtype": "Check", - "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": "Private", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "read_only": 1 + }, + { + "fieldname": "fields", + "fieldtype": "Code", + "label": "Fields", + "options": "JSON", + "read_only": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "show_labels", + "fieldtype": "Check", + "label": "Show Labels", + "read_only": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-09-05 14:22:27.664645", + "links": [], + "modified": "2022-04-13 12:10:20.284367", "modified_by": "Administrator", "module": "Desk", "name": "Kanban Board", - "name_case": "", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, + "read": 1, + "role": "All" + }, + { + "create": 1, + "delete": 1, + "if_owner": 1, + "read": 1, + "role": "All", + "write": 1 + }, + { "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, - "role": "All", - "set_user_permissions": 0, + "role": "System Manager", "share": 1, - "submit": 0, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, + "read_only": 1, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/desk/doctype/kanban_board/kanban_board.py b/frappe/desk/doctype/kanban_board/kanban_board.py index e864f68728..381f71438c 100644 --- a/frappe/desk/doctype/kanban_board/kanban_board.py +++ b/frappe/desk/doctype/kanban_board/kanban_board.py @@ -24,7 +24,7 @@ class KanbanBoard(Document): def validate_column_name(self): for column in self.columns: if not column.column_name: - frappe.msgprint(frappe._("Column Name cannot be empty"), raise_exception=True) + frappe.msgprint(_("Column Name cannot be empty"), raise_exception=True) def get_permission_query_conditions(user): @@ -92,7 +92,6 @@ def update_order(board_name, order): updated_cards = [] for col_name, cards in order_dict.items(): - order_list = [] for card in cards: column = frappe.get_value(doctype, {"name": card}, fieldname) if column != col_name: @@ -246,3 +245,22 @@ def set_indicator(board_name, column_name, indicator): board.save() return board + + +@frappe.whitelist() +def save_settings(board_name: str, settings: str) -> Document: + settings = json.loads(settings) + doc = frappe.get_doc("Kanban Board", board_name) + + fields = settings["fields"] + if not isinstance(fields, str): + fields = json.dumps(fields) + + doc.fields = fields + doc.show_labels = settings["show_labels"] + doc.save() + + resp = doc.as_dict() + resp["fields"] = frappe.parse_json(resp["fields"]) + + return resp diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 069f353368..35e387b8d8 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -1580,15 +1580,22 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { } if (frappe.user.has_role("System Manager")) { - items.push({ - label: __("List Settings", null, "Button in list view menu"), - action: () => this.show_list_settings(), - standard: true, - }); + if (this.get_view_settings) { + items.push(this.get_view_settings()); + } } + return items; } + get_view_settings() { + return { + label: __("List Settings", null, "Button in list view menu"), + action: () => this.show_list_settings(), + standard: true, + }; + } + show_list_settings() { frappe.model.with_doctype(this.doctype, () => { new ListSettings({ diff --git a/frappe/public/js/frappe/views/kanban/kanban_board.js b/frappe/public/js/frappe/views/kanban/kanban_board.js index 58d58b27fc..64e90f5326 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_board.js +++ b/frappe/public/js/frappe/views/kanban/kanban_board.js @@ -630,8 +630,6 @@ frappe.provide("frappe.views"); if(!card) return; make_dom(); render_card_meta(); - add_task_link(); - // edit_card_title(); } function make_dom() { @@ -640,12 +638,35 @@ frappe.provide("frappe.views"); title: frappe.utils.html2text(card.title), disable_click: card._disable_click ? 'disable-click' : '', creation: card.creation, + doc_content: get_doc_content(card), image_url: cur_list.get_image_url(card), + form_link: frappe.utils.get_form_link(card.doctype, card.name) }; + self.$card = $(frappe.render_template('kanban_card', opts)) .appendTo(wrapper); } + function get_doc_content(card) { + let fields = []; + for (let field_name of cur_list.board.fields) { + let field = ( + frappe.meta.get_docfield(card.doctype, field_name, card.name) + || frappe.model.get_std_field(field_name) + ); + let label = cur_list.board.show_labels ? `${__(field.label)}: ` : ''; + let value = frappe.format(card.doc[field_name], field); + fields.push(` +
+ ${label} + ${value} +
+ `); + } + + return fields.join(""); + } + function get_tags_html(card) { return card.tags ? `
@@ -688,12 +709,6 @@ frappe.provide("frappe.views"); .find('.kanban-assignments').append($assignees_group); } - function add_task_link() { - let task_link = frappe.utils.get_form_link(card.doctype, card.name); - self.$card.find('.kanban-card-redirect') - .attr('href', task_link); - } - function get_assignees_group() { return frappe.avatar_group(card.assigned_list, 3, { css_class: 'avatar avatar-small', @@ -744,7 +759,7 @@ frappe.provide("frappe.views"); assigned_list: card.assigned_list || assigned_list, comment_count: card.comment_count || comment_count, color: card.color || null, - doc: doc + doc: doc || card }; } diff --git a/frappe/public/js/frappe/views/kanban/kanban_card.html b/frappe/public/js/frappe/views/kanban/kanban_card.html index 88cf366ccc..ed8e4cc6ff 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_card.html +++ b/frappe/public/js/frappe/views/kanban/kanban_card.html @@ -1,23 +1,24 @@
- -
- {% if(image_url) { %} -
- {{title}} -
- {% } %} -
diff --git a/frappe/public/js/frappe/views/kanban/kanban_settings.js b/frappe/public/js/frappe/views/kanban/kanban_settings.js new file mode 100644 index 0000000000..b41922e544 --- /dev/null +++ b/frappe/public/js/frappe/views/kanban/kanban_settings.js @@ -0,0 +1,264 @@ +export default class KanbanSettings { + constructor({ kanbanview, doctype, meta, settings }) { + if (!doctype) { + frappe.throw(__("DocType required")); + } + + this.kanbanview = kanbanview; + this.doctype = doctype; + this.meta = meta; + this.settings = settings; + this.dialog = null; + this.fields = this.settings && this.settings.fields; + + frappe.model.with_doctype("List View Settings", () => { + this.make(); + this.get_fields(); + this.setup_fields(); + this.setup_remove_fields(); + this.add_new_fields(); + this.show_dialog(); + }); + } + + make() { + this.dialog = new frappe.ui.Dialog({ + title: __("{0} Settings", [__(this.doctype)]), + fields: [ + { + fieldname: "show_labels", + label: __("Show Labels"), + fieldtype: "Check", + }, + { + fieldname: "fields_html", + fieldtype: "HTML" + }, + { + fieldname: "fields", + fieldtype: "Code", + hidden: 1 + } + ] + }); + this.dialog.set_values(this.settings); + this.dialog.set_primary_action(__("Save"), () => { + frappe.show_alert({ + message: __("Saving"), + indicator: "green" + }); + + frappe.call({ + method: + "frappe.desk.doctype.kanban_board.kanban_board.save_settings", + args: { + board_name: this.settings.name, + settings: this.dialog.get_values() + }, + callback: r => { + this.kanbanview.board = r.message; + this.kanbanview.render(); + this.dialog.hide(); + } + }); + }); + } + + refresh() { + this.setup_fields(); + this.add_new_fields(); + this.setup_remove_fields(); + } + + show_dialog() { + if (!this.settings.fields) { + this.update_fields(); + } + + this.dialog.show(); + } + + setup_fields() { + const fields_html = this.dialog.get_field("fields_html"); + const wrapper = fields_html.$wrapper[0]; + let fields = ""; + + for (let fieldname of this.fields) { + let field = this.get_docfield(fieldname); + + fields += ` +
+ +
+
+ ${frappe.utils.icon("drag", "xs", "", "", "sortable-handle")} +
+
+ ${__(field.label)} +
+ +
+
`; + } + + fields_html.html(` +
+
+ +
+
+ ${fields} +
+

+ + + Add / Remove Fields + +

+
+ `); + + new Sortable( + wrapper.getElementsByClassName("control-input-wrapper")[0], + { + handle: ".sortable-handle", + draggable: ".sortable", + onUpdate: params => { + this.fields.splice( + params.newIndex, + 0, + this.fields.splice(params.oldIndex, 1)[0] + ); + this.dialog.set_value( + "fields", + JSON.stringify(this.fields) + ); + this.refresh(); + } + } + ); + } + + add_new_fields() { + let add_new_fields = this.get_dialog_fields_wrapper().getElementsByClassName( + "add-new-fields" + )[0]; + add_new_fields.onclick = () => this.show_column_selector(); + } + + setup_remove_fields() { + let remove_fields = this.get_dialog_fields_wrapper().getElementsByClassName( + "remove-field" + ); + + for (let idx = 0; idx < remove_fields.length; idx++) { + remove_fields.item(idx).onclick = () => + this.remove_fields( + remove_fields.item(idx).getAttribute("data-fieldname") + ); + } + } + + get_dialog_fields_wrapper() { + return this.dialog.get_field("fields_html").$wrapper[0]; + } + + remove_fields(fieldname) { + this.fields = this.fields.filter(field => field !== fieldname); + this.dialog.set_value("fields", JSON.stringify(this.fields)); + this.refresh(); + } + + update_fields() { + const wrapper = this.dialog.get_field("fields_html").$wrapper[0]; + let fields_order = wrapper.getElementsByClassName("fields_order"); + this.fields = []; + + for (let idx = 0; idx < fields_order.length; idx++) { + this.fields.push( + fields_order.item(idx).getAttribute("data-fieldname") + ); + } + + this.dialog.set_value("fields", JSON.stringify(this.fields)); + } + + show_column_selector() { + let dialog = new frappe.ui.Dialog({ + title: __("{0} Fields", [__(this.doctype)]), + fields: [ + { + label: __("Select Fields"), + fieldtype: "MultiCheck", + fieldname: "fields", + options: this.get_multiselect_fields(), + columns: 2 + } + ] + }); + dialog.set_primary_action(__("Save"), () => { + this.fields = dialog.get_values().fields || []; + this.dialog.set_value("fields", JSON.stringify(this.fields)); + this.refresh(); + dialog.hide(); + }); + dialog.show(); + } + + get_fields() { + this.fields = this.settings.fields; + this.fields.uniqBy(f => f.fieldname); + } + + get_docfield(field_name) { + return ( + frappe.meta.get_docfield(this.doctype, field_name) || + frappe.model.get_std_field(field_name) + ); + } + + get_multiselect_fields() { + const ignore_fields = [ + "idx", + "lft", + "rgt", + "old_parent", + "_user_tags", + "_liked_by", + "_comments", + "_assign", + this.meta.title_field || "name" + ]; + + const ignore_fieldtypes = [ + "Attach Image", + "Text Editor", + "HTML Editor", + "Code", + "Color", + ...frappe.model.no_value_type + ]; + + return frappe.model.std_fields + .concat(this.kanbanview.get_fields_in_list_view()) + .filter( + field => + !ignore_fields.includes(field.fieldname) && + !ignore_fieldtypes.includes(field.fieldtype) + ) + .map(field => { + return { + label: __(field.label), + value: field.fieldname, + checked: this.fields.includes(field.fieldname) + }; + }); + } +} diff --git a/frappe/public/js/frappe/views/kanban/kanban_view.js b/frappe/public/js/frappe/views/kanban/kanban_view.js index 3bf3a16189..b546365fd9 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_view.js +++ b/frappe/public/js/frappe/views/kanban/kanban_view.js @@ -1,3 +1,5 @@ +import KanbanSettings from "./kanban_settings"; + frappe.provide('frappe.views'); frappe.views.KanbanView = class KanbanView extends frappe.views.ListView { @@ -57,6 +59,7 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView { .then(board => { this.board = board; this.board.filters_array = JSON.parse(this.board.filters || '[]'); + this.board.fields = JSON.parse(this.board.fields || '[]'); this.filters = this.board.filters_array; }); } @@ -187,6 +190,25 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView { }; } + get_view_settings() { + return { + label: __("Kanban Settings", null, "Button in kanban view menu"), + action: () => this.show_kanban_settings(), + standard: true, + }; + } + + show_kanban_settings() { + frappe.model.with_doctype(this.doctype, () => { + new KanbanSettings({ + kanbanview: this, + doctype: this.doctype, + settings: this.board, + meta: frappe.get_meta(this.doctype) + }); + }); + } + get required_libs() { return [ 'assets/frappe/js/lib/fluxify.min.js', diff --git a/frappe/public/scss/desk/kanban.scss b/frappe/public/scss/desk/kanban.scss index 41bd0b2859..8f286b7b35 100644 --- a/frappe/public/scss/desk/kanban.scss +++ b/frappe/public/scss/desk/kanban.scss @@ -139,6 +139,7 @@ } .kanban-cards { + height: 100%; max-height: calc(100vh - 250px); margin: -5px; padding: 5px; @@ -149,39 +150,120 @@ &::-webkit-scrollbar { display: none; } - } - .kanban-card { - @include flex(flex, space-between, null, column); - margin-top: var(--margin-sm); - min-height: 100px; - @include card( - $padding: 0, - $background-color: var(--kanban-card-bg) - ); - .kanban-card-body { - padding: var(--padding-sm); + .kanban-card-wrapper { + position: relative; + display: block; + + &:last-child .kanban-card { + margin-bottom: var(--margin-xl); + } + .kanban-card { + @include flex(flex, space-between, null, column); + margin-top: var(--margin-sm); + min-height: 100px; + @include card( + $padding: 0, + $background-color: var(--kanban-card-bg) + ); + + .kanban-image { + height: 125px; + + img { + border-radius: var(--border-radius) var(--border-radius) 0 0; + object-position: top; + object-fit: cover; + margin: 0 auto; + height: 100%; + width: 100%; + min-width: 100%; + color: transparent; + position: relative; + } + + @include broken-img( + $height: 125px, + $top: -4px, + ) + } + + .kanban-card-body { + cursor: grab; + padding: var(--padding-sm); + + .kanban-title-area { + margin-bottom: 12px; + max-width: 90%; + font-size: var(--text-md); + font-weight: 500; + + .kanban-card-doc { + .text-muted div { + display: inline; + } + } + + .kanban-card-creation { + font-size: var(--text-md); + color: var(--text-muted); + margin-top: var(--margin-xs); + } + } + + .kanban-card-meta { + + .list-comment-count { + width: 30px; + } + + .like-action:not(.liked) { + .icon use { + stroke: var(--text-muted); + } + } + + .kanban-tags { + font-size: var(--text-sm); + margin-bottom: 8px; + + .tag-pill { + border-radius: 100px; + height: 22px; + width: auto; + padding: 2px 8px; + margin-bottom: var(--margin-xs); + margin-right: var(--margin-xs); + } + } + + .kanban-assignments { + display: flex; + float: right; + + .avatar { + cursor: default; + width: 22px; + height: 22px; + } + + .avatar-action { + width: 22px; + height: 22px; + + .icon { + width: 12px; + height: 12px; + } + } + } + } + } + } } } } - .kanban-card-wrapper { - position: relative; - - .kanban-card-redirect { - display: block; - - &:hover, - &:focus { - text-decoration: none; - } - } - - &:last-child .kanban-card { - margin-bottom: var(--margin-xl); - } - } - .kanban-card:hover, .new-card-area, .edit-card-area { @@ -189,7 +271,6 @@ } .kanban-card-wrapper:hover { - cursor: pointer; text-decoration: none; .kanban-card-edit { @@ -197,43 +278,6 @@ } } - .kanban-title-area { - margin-bottom: 12px; - - .kanban-card-title { - max-width: 90%; - font-size: var(--text-md); - font-weight: 500; - } - - .kanban-card-creation { - font-size: var(--text-md); - color: var(--text-muted); - margin-top: var(--margin-xs); - } - } - - .kanban-image { - height: 125px; - - img { - border-radius: var(--border-radius) var(--border-radius) 0 0; - object-position: top; - object-fit: cover; - margin: 0 auto; - height: 100%; - width: 100%; - min-width: 100%; - color: transparent; - position: relative; - } - - @include broken-img( - $height: 125px, - $top: -4px, - ) - } - .kanban-card-edit { position: absolute; right: 10px; @@ -291,54 +335,6 @@ } } - .kanban-card-meta { - - .list-comment-count { - width: 30px; - } - - .like-action:not(.liked) { - .icon use { - stroke: var(--text-muted); - } - } - - .kanban-tags { - font-size: var(--text-sm); - margin-bottom: 8px; - - .tag-pill { - border-radius: 100px; - height: 22px; - width: auto; - padding: 2px 8px; - margin-bottom: var(--margin-xs); - margin-right: var(--margin-xs); - } - } - - .kanban-assignments { - display: flex; - float: right; - - .avatar { - cursor: default; - width: 22px; - height: 22px; - } - - .avatar-action { - width: 22px; - height: 22px; - - .icon { - width: 12px; - height: 12px; - } - } - } - } - .kanban-empty-state { width: 100%; line-height: 400px; From 17aeec04c85168e84d94dc8205134a959f74cedd Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 18 Apr 2022 16:45:39 +0530 Subject: [PATCH 06/57] chore: do not publish any assets --- .releaserc | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.releaserc b/.releaserc index 2b3f4975db..530a6c0767 100644 --- a/.releaserc +++ b/.releaserc @@ -19,12 +19,6 @@ "message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}" } ], - [ - "@semantic-release/github", { - "assets": [ - {"path": "dist/*"}, - ] - } - ] + "@semantic-release/github" ] } \ No newline at end of file From bf21d2fe2a3dee9ec68556a05ff795c2ea5c1779 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 18 Apr 2022 16:46:14 +0530 Subject: [PATCH 07/57] chore: use ubuntu latest --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d7cf89f017..52fc595c96 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: jobs: release: name: Release - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest steps: - name: Checkout Entire Repository uses: actions/checkout@v2 From 57a55e4225821c6b9034b8633fe86eac624c002b Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 18 Apr 2022 15:19:25 +0530 Subject: [PATCH 08/57] feat(minor): Add document reference to Error Log and doc.log_error --- frappe/__init__.py | 29 ++- .../doctype/auto_repeat/auto_repeat.py | 2 +- .../doctype/communication/communication.py | 3 +- frappe/core/doctype/communication/email.py | 2 +- .../core/doctype/data_import/data_import.py | 2 +- frappe/core/doctype/error_log/error_log.json | 203 ++++++------------ frappe/core/doctype/file/file.py | 4 +- .../prepared_report/prepared_report.py | 2 +- frappe/core/doctype/user/user.py | 2 +- frappe/desk/desktop.py | 8 +- .../notification_log/notification_log.py | 2 +- .../system_console/system_console.json | 8 + frappe/desk/query_report.py | 2 +- .../auto_email_report/auto_email_report.py | 3 +- .../doctype/email_account/email_account.py | 9 +- .../email/doctype/email_queue/email_queue.py | 15 +- frappe/email/doctype/newsletter/newsletter.py | 10 +- .../doctype/notification/notification.py | 8 +- frappe/email/queue.py | 2 +- frappe/email/receive.py | 4 +- .../doctype/event_consumer/event_consumer.py | 2 +- frappe/model/document.py | 8 + frappe/model/workflow.py | 5 +- .../energy_point_rule/energy_point_rule.py | 4 + frappe/utils/print_format.py | 6 +- .../doctype/web_form/templates/web_form.html | 2 + 26 files changed, 152 insertions(+), 195 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 10c8afbf23..07a580944f 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -2068,26 +2068,33 @@ def logger( ) -def log_error(message=None, title=_("Error")): - """Log error to Error Log""" +def log_error(title=None, message=None, reference_doctype=None, reference_name=None): + '''Log error to Error Log''' - # AI ALERT: + # Parameter ALERT: # the title and message may be swapped # the better API for this is log_error(title, message), and used in many cases this way # this hack tries to be smart about whats a title (single line ;-)) and fixes it if message: - if "\n" in title: - error, title = title, message + if '\n' in title: # traceback sent as title + traceback, title = title, message else: - error = message - else: - error = get_traceback() + traceback = message - return get_doc(dict(doctype="Error Log", error=as_unicode(error), method=title)).insert( - ignore_permissions=True - ) + if not traceback: + traceback = get_traceback() + if not title: + title = 'Error' + + return get_doc(dict( + doctype='Error Log', + error=as_unicode(traceback), + method=title, + reference_doctype=reference_doctype, + reference_name=reference_name + )).insert(ignore_permissions=True) def get_desk_link(doctype, name): html = ( diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 5ff9e9f3ab..4b2f83f4b6 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -189,7 +189,7 @@ class AutoRepeat(Document): if self.notify_by_email and self.recipients: self.send_notification(new_doc) except Exception: - error_log = frappe.log_error(frappe.get_traceback(), _("Auto Repeat Document Creation Failure")) + error_log = self.log_error('Auto repeat failed') self.disable_auto_repeat() diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index 409c4c0956..eeddb7c7a1 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -450,8 +450,7 @@ def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[st contact.insert(ignore_permissions=True) contact_name = contact.name except Exception: - traceback = frappe.get_traceback() - frappe.log_error(traceback) + contact.log_error('Unable to add contact') if contact_name: contacts.append(contact_name) diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index 5737572194..d47b15d360 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -248,7 +248,7 @@ def mark_email_as_seen(name: str = None): frappe.db.commit() # nosemgrep: this will be called in a GET request except Exception: - frappe.log_error(frappe.get_traceback()) + frappe.log_error('Unable to mark as seen', None, 'Communication', name) finally: frappe.response.update( diff --git a/frappe/core/doctype/data_import/data_import.py b/frappe/core/doctype/data_import/data_import.py index 295f7e79ba..3693ea53a4 100644 --- a/frappe/core/doctype/data_import/data_import.py +++ b/frappe/core/doctype/data_import/data_import.py @@ -113,7 +113,7 @@ def start_import(data_import): except Exception: frappe.db.rollback() data_import.db_set("status", "Error") - frappe.log_error(title=data_import.name) + data_import.log_error('Data import failed') finally: frappe.flags.in_import = False diff --git a/frappe/core/doctype/error_log/error_log.json b/frappe/core/doctype/error_log/error_log.json index 35ca3ceeef..e643bbb090 100644 --- a/frappe/core/doctype/error_log/error_log.json +++ b/frappe/core/doctype/error_log/error_log.json @@ -1,148 +1,75 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "", - "beta": 0, - "creation": "2013-01-16 13:09:40", - "custom": 0, - "description": "Log of Scheduler Errors", - "docstatus": 0, - "doctype": "DocType", - "document_type": "System", - "editable_grid": 0, - "engine": "MyISAM", + "actions": [], + "creation": "2013-01-16 13:09:40", + "doctype": "DocType", + "document_type": "System", + "engine": "MyISAM", + "field_order": [ + "seen", + "method", + "error", + "reference_doctype", + "reference_name" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fieldname": "seen", - "fieldtype": "Check", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Seen", - "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": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "seen", + "fieldtype": "Check", + "hidden": 1, + "label": "Seen" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "method", - "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": "Title", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "method", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Title", + "read_only": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "error", - "fieldtype": "Code", - "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": "Error", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "error", + "fieldtype": "Code", + "label": "Error", + "read_only": 1 + }, + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Reference DocType", + "options": "DocType", + "search_index": 1 + }, + { + "fieldname": "reference_name", + "fieldtype": "Data", + "label": "Reference Name" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-warning-sign", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2021-10-25 12:21:44.292471", - "modified_by": "Administrator", - "module": "Core", - "name": "Error Log", - "owner": "Administrator", + ], + "icon": "fa fa-warning-sign", + "idx": 1, + "links": [], + "modified": "2022-04-18 14:51:30.604304", + "modified_by": "Administrator", + "module": "Core", + "name": "Error Log", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_order": "ASC", - "track_seen": 0 -} + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "ASC", + "states": [] +} \ No newline at end of file diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index d8b45cf043..3547a03832 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -1043,7 +1043,7 @@ def attach_files_to_document(doc, event): ): return - frappe.get_doc( + file_doc = frappe.get_doc( doctype="File", file_url=value, attached_to_name=doc.name, @@ -1052,4 +1052,4 @@ def attach_files_to_document(doc, event): folder="Home/Attachments", ).insert() except Exception: - frappe.log_error(title=_("Error Attaching File")) + file_doc.log_error("Error Attaching File") diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py index c3122fe52f..dbdba19fbe 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.py +++ b/frappe/core/doctype/prepared_report/prepared_report.py @@ -47,7 +47,7 @@ def run_background(prepared_report): instance.save(ignore_permissions=True) except Exception: - frappe.log_error(frappe.get_traceback()) + report.log_error('Prepared report failed') instance = frappe.get_doc("Prepared Report", prepared_report) instance.status = "Error" instance.error_message = frappe.get_traceback() diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index c90cbf1fce..ec7374d72a 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -265,7 +265,7 @@ class User(Document): except frappe.OutgoingEmailError: # email server not set, don't send email - frappe.log_error(frappe.get_traceback()) + self.log_error('Unable to send new password notification') @Document.hook def validate_reset_password(self): diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index 385151f754..ae34cf3df2 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -338,7 +338,7 @@ def get_desktop_page(page): "onboardings": workspace.onboardings, } except DoesNotExistError: - frappe.log_error(frappe.get_traceback()) + frappe.log_error('Workspace Missing') return {} @@ -469,10 +469,8 @@ def save_new_widget(doc, page, blocks, new_widgets): page: {0} config: {1} exception: {2} - """.format( - page, json_config, e - ) - frappe.log_error(log, _("Could not save customization")) + """.format(page, json_config, e) + doc.log_error("Could not save customization", log) return False return True diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py index 1a466ea78b..011f3e22ff 100644 --- a/frappe/desk/doctype/notification_log/notification_log.py +++ b/frappe/desk/doctype/notification_log/notification_log.py @@ -20,7 +20,7 @@ class NotificationLog(Document): try: send_notification_email(self) except frappe.OutgoingEmailError: - frappe.log_error(message=frappe.get_traceback(), title=_("Failed to send notification email")) + self.log_error(_("Failed to send notification email")) def get_permission_query_conditions(for_user): diff --git a/frappe/desk/doctype/system_console/system_console.json b/frappe/desk/doctype/system_console/system_console.json index c92b2005ed..bbb815e30e 100644 --- a/frappe/desk/doctype/system_console/system_console.json +++ b/frappe/desk/doctype/system_console/system_console.json @@ -1,7 +1,11 @@ { "actions": [ { +<<<<<<< HEAD "action": "app/console-log", +======= + "action": "/app/console-log", +>>>>>>> 6d82805831 (feat(minor): Add document reference to Error Log and doc.log_error) "action_type": "Route", "label": "Logs" }, @@ -86,7 +90,11 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], +<<<<<<< HEAD "modified": "2022-04-09 16:35:32.345542", +======= + "modified": "2022-04-15 14:15:58.398590", +>>>>>>> 6d82805831 (feat(minor): Add document reference to Error Log and doc.log_error) "modified_by": "Administrator", "module": "Desk", "name": "System Console", diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 7de8ccabbf..d1f63a6199 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -314,7 +314,7 @@ def get_prepared_report_result(report, filters, dn="", user=None): latest_report_data = {"columns": columns, "result": data} except Exception: - frappe.log_error(frappe.get_traceback()) + doc.log_error(frappe.get_traceback()) frappe.delete_doc("Prepared Report", doc.name) frappe.db.commit() doc = None diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py index f4fdcf4275..108cc46792 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.py +++ b/frappe/email/doctype/auto_email_report/auto_email_report.py @@ -255,8 +255,7 @@ def send_daily(): try: auto_email_report.send() except Exception as e: - frappe.log_error(e, _("Failed to send {0} Auto Email Report").format(auto_email_report.name)) - + auto_email_report.log_error('Failed to send {0} Auto Email Report'.format(auto_email_report.name)) def send_monthly(): """Check reports to be sent monthly""" diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index e60be0d965..65586de31d 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -473,7 +473,7 @@ class EmailAccount(Document): frappe.db.rollback() except Exception: frappe.db.rollback() - frappe.log_error(title="EmailAccount.receive") + self.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()) @@ -521,7 +521,7 @@ class EmailAccount(Document): # close connection to mailserver email_server.logout() except Exception: - frappe.log_error(title=_("Error while connecting to email account {0}").format(self.name)) + self.log_error(title=_("Error while connecting to email account {0}").format(self.name)) return [] return mails @@ -667,7 +667,7 @@ class EmailAccount(Document): try: email_server = self.get_incoming_server(in_receive=True) except Exception: - frappe.log_error(title=_("Error while connecting to email account {0}").format(self.name)) + self.log_error(title=_("Error while connecting to email account {0}").format(self.name)) if not email_server: return @@ -679,8 +679,7 @@ class EmailAccount(Document): message = safe_encode(message) email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message) except Exception: - frappe.log_error(title="EmailAccount.append_email_to_sent_folder") - + self.log_error(title="EmailAccount.append_email_to_sent_folder") @frappe.whitelist() def get_append_to( diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py index 38577eeb97..b3e52ef1e2 100644 --- a/frappe/email/doctype/email_queue/email_queue.py +++ b/frappe/email/doctype/email_queue/email_queue.py @@ -198,10 +198,7 @@ class SendMailContext: traceback_string = "".join(traceback.format_tb(exc_tb)) traceback_string += f"\n Queue Name: {self.queue_doc.name}" - if self.is_background_task: - frappe.log_error(title="frappe.email.queue.flush", message=traceback_string) - else: - frappe.log_error(message=traceback_string) + self.queue_doc.log_error('Email sending failed', traceback_string) @property def smtp_session(self): @@ -625,12 +622,10 @@ class QueueBuilder: mail_to_string = cstr(mail.as_string()) except frappe.InvalidEmailAddressError: # bad Email Address - don't add to queue - frappe.log_error( - "Invalid Email ID Sender: {0}, Recipients: {1}, \nTraceback: {2} ".format( - self.sender, ", ".join(self.final_recipients()), traceback.format_exc() - ), - "Email Not Sent", - ) + self.log_error( + title = 'Invalid email address', + message = 'Invalid email address Sender: {0}, Recipients: {1}, \nTraceback: {2} ' + .format(self.sender, ', '.join(self.final_recipients()), traceback.format_exc())) return d = { diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index 45a4539866..e3726ed5a6 100644 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -329,19 +329,25 @@ def send_scheduled_email(): pluck="name", ) - for newsletter in scheduled_newsletter: + for newsletter_name in scheduled_newsletter: try: - frappe.get_doc("Newsletter", newsletter).queue_all() + newsletter = frappe.get_doc("Newsletter", newsletter_name) + newsletter.queue_all() except Exception: frappe.db.rollback() # wasn't able to send emails :( +<<<<<<< HEAD frappe.db.set_value("Newsletter", newsletter, "email_sent", 0) message = ( f"Newsletter {newsletter} failed to send" "\n\n" f"Traceback: {frappe.get_traceback()}" ) frappe.log_error(title="Send Newsletter", message=message) +======= + frappe.db.set_value("Newsletter", newsletter_name, "email_sent", 0) + newsletter.log_error('Failed to send newsletter') +>>>>>>> 6d82805831 (feat(minor): Add document reference to Error Log and doc.log_error) if not frappe.flags.in_test: frappe.db.commit() diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index 5c27eb95eb..e868c0ceb6 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -141,7 +141,7 @@ def get_context(context): self.create_system_notification(doc, context) except: - frappe.log_error(title="Failed to send notification", message=frappe.get_traceback()) + self.log_error('Failed to send Notification') if self.set_property_after_alert: allow_update = True @@ -168,7 +168,7 @@ def get_context(context): doc.save(ignore_permissions=True) doc.flags.in_notification_update = False except Exception: - frappe.log_error(title="Document update failed", message=frappe.get_traceback()) + self.log_error('Document update failed') def create_system_notification(self, doc, context): subject = self.subject @@ -432,8 +432,8 @@ def evaluate_alert(doc, alert, event): if event == "Value Change" and not doc.is_new(): if not frappe.db.has_column(doc.doctype, alert.value_changed): - alert.db_set("enabled", 0) - frappe.log_error("Notification {0} has been disabled due to missing field".format(alert.name)) + alert.db_set('enabled', 0) + alert.log_error('Notification {0} has been disabled due to missing field'.format(alert.name)) return doc_before_save = doc.get_doc_before_save() diff --git a/frappe/email/queue.py b/frappe/email/queue.py index b0a3b0583b..ac873182a7 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -170,7 +170,7 @@ def flush(from_test=False): is_background_task = not from_test func(email_queue_name=row.name, is_background_task=is_background_task) except Exception: - frappe.log_error() + frappe.get_doc('Email Queue', row.name).log_error() def get_queue(): diff --git a/frappe/email/receive.py b/frappe/email/receive.py index 4a6db65a84..80c413faa1 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -123,7 +123,7 @@ class EmailServer: except _socket.error: # log performs rollback and logs error in Error Log - frappe.log_error("receive.connect_pop") + self.log_error("POP: Unable to connect") # Invalid mail server -- due to refusing connection frappe.msgprint(_("Invalid Mail Server. Please rectify and try again.")) @@ -306,7 +306,7 @@ class EmailServer: else: # log performs rollback and logs error in Error Log - frappe.log_error("receive.get_messages", self.make_error_msg(msg_num, incoming_mail)) + self.log_error("Unable to fetch email", self.make_error_msg(msg_num, incoming_mail)) self.errors = True frappe.db.rollback() diff --git a/frappe/event_streaming/doctype/event_consumer/event_consumer.py b/frappe/event_streaming/doctype/event_consumer/event_consumer.py index 287a1fca03..dd3d2ec74c 100644 --- a/frappe/event_streaming/doctype/event_consumer/event_consumer.py +++ b/frappe/event_streaming/doctype/event_consumer/event_consumer.py @@ -213,5 +213,5 @@ def has_consumer_access(consumer, update_log): else: return frappe.safe_eval(condition, frappe._dict(doc=doc)) except Exception as e: - frappe.log_error(title="has_consumer_access error", message=e) + consumer.log_error('has_consumer_access error') return False diff --git a/frappe/model/document.py b/frappe/model/document.py index 58c406607d..a7b59ae749 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -1362,6 +1362,14 @@ class Document(BaseDocument): ).insert(ignore_permissions=True) frappe.local.flags.commit = True + def log_error(self, title=None, message=None): + '''Helper function to create an Error Log''' + return frappe.log_error( + message = message, + title = title, + reference_doctype = self.doctype, + reference_name = self.name) + def get_signature(self): """Returns signature (hash) for private URL.""" return hashlib.sha224(get_datetime_str(self.creation).encode()).hexdigest() diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py index 0edffaf2fb..092ad4f764 100644 --- a/frappe/model/workflow.py +++ b/frappe/model/workflow.py @@ -254,8 +254,9 @@ def bulk_workflow_approval(docnames, doctype, action): frappe.db.rollback() frappe.log_error( - frappe.get_traceback(), - "Workflow {0} threw an error for {1} {2}".format(action, doctype, docname), + title = "Workflow {0} threw an error for {1} {2}".format(action, doctype, docname), + reference_doctype = 'Workflow', + reference_name = action ) finally: if not message_dict: diff --git a/frappe/social/doctype/energy_point_rule/energy_point_rule.py b/frappe/social/doctype/energy_point_rule/energy_point_rule.py index a36581ec4c..3767a9564e 100644 --- a/frappe/social/doctype/energy_point_rule/energy_point_rule.py +++ b/frappe/social/doctype/energy_point_rule/energy_point_rule.py @@ -57,7 +57,11 @@ class EnergyPointRule(Document): self.apply_only_once, ) except Exception as e: +<<<<<<< HEAD frappe.log_error(frappe.get_traceback(), "apply_energy_point") +======= + self.log_error('Energy points failed') +>>>>>>> 6d82805831 (feat(minor): Add document reference to Error Log and doc.log_error) def rule_condition_satisfied(self, doc): if self.for_doc_event == "New": diff --git a/frappe/utils/print_format.py b/frappe/utils/print_format.py index 35842217d1..2488fae0c9 100644 --- a/frappe/utils/print_format.py +++ b/frappe/utils/print_format.py @@ -92,7 +92,11 @@ def download_multi_pdf(doctype, name, format=None, no_letterhead=False, options= pdf_options=options, ) except Exception: - frappe.log_error("Permission Error on doc {} of doctype {}".format(doc_name, doctype_name)) + frappe.log_error( + title = 'Error in Multi PDF download', + message = "Permission Error on doc {} of doctype {}".format(doc_name, doctype_name), + reference_doctype = doctype_name, + reference_name = doc_name) frappe.local.response.filename = "{}.pdf".format(name) frappe.local.response.filecontent = read_multi_pdf(output) diff --git a/frappe/website/doctype/web_form/templates/web_form.html b/frappe/website/doctype/web_form/templates/web_form.html index 2a02aa5f2b..6e0374abdb 100644 --- a/frappe/website/doctype/web_form/templates/web_form.html +++ b/frappe/website/doctype/web_form/templates/web_form.html @@ -70,6 +70,8 @@ data-web-form="{{ name }}" data-web-form-doctype="{{ doc_type }}" data-login-req
{% include 'templates/includes/comments/comments.html' %}
+{%- else -%} +
{%- endif %} {# comments #} {% endblock page_content %} From d55986d56dbd78497dd42c48e3e02dedb0be16e5 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 18 Apr 2022 15:47:05 +0530 Subject: [PATCH 09/57] fix(conflicts): fix conflicts' --- frappe/desk/doctype/system_console/system_console.json | 10 +--------- frappe/desk/query_report.py | 2 +- frappe/email/doctype/newsletter/newsletter.py | 10 +--------- .../doctype/energy_point_rule/energy_point_rule.py | 6 +----- 4 files changed, 4 insertions(+), 24 deletions(-) diff --git a/frappe/desk/doctype/system_console/system_console.json b/frappe/desk/doctype/system_console/system_console.json index bbb815e30e..a851831909 100644 --- a/frappe/desk/doctype/system_console/system_console.json +++ b/frappe/desk/doctype/system_console/system_console.json @@ -1,11 +1,7 @@ { "actions": [ { -<<<<<<< HEAD - "action": "app/console-log", -======= "action": "/app/console-log", ->>>>>>> 6d82805831 (feat(minor): Add document reference to Error Log and doc.log_error) "action_type": "Route", "label": "Logs" }, @@ -90,11 +86,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], -<<<<<<< HEAD - "modified": "2022-04-09 16:35:32.345542", -======= "modified": "2022-04-15 14:15:58.398590", ->>>>>>> 6d82805831 (feat(minor): Add document reference to Error Log and doc.log_error) "modified_by": "Administrator", "module": "Desk", "name": "System Console", @@ -114,4 +106,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index d1f63a6199..894e82d117 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -314,7 +314,7 @@ def get_prepared_report_result(report, filters, dn="", user=None): latest_report_data = {"columns": columns, "result": data} except Exception: - doc.log_error(frappe.get_traceback()) + doc.log_error("Prepared report failed") frappe.delete_doc("Prepared Report", doc.name) frappe.db.commit() doc = None diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index e3726ed5a6..6aa881ed5c 100644 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -338,16 +338,8 @@ def send_scheduled_email(): frappe.db.rollback() # wasn't able to send emails :( -<<<<<<< HEAD - frappe.db.set_value("Newsletter", newsletter, "email_sent", 0) - message = ( - f"Newsletter {newsletter} failed to send" "\n\n" f"Traceback: {frappe.get_traceback()}" - ) - frappe.log_error(title="Send Newsletter", message=message) -======= frappe.db.set_value("Newsletter", newsletter_name, "email_sent", 0) - newsletter.log_error('Failed to send newsletter') ->>>>>>> 6d82805831 (feat(minor): Add document reference to Error Log and doc.log_error) + newsletter.log_error("Failed to send newsletter") if not frappe.flags.in_test: frappe.db.commit() diff --git a/frappe/social/doctype/energy_point_rule/energy_point_rule.py b/frappe/social/doctype/energy_point_rule/energy_point_rule.py index 3767a9564e..9d393dde48 100644 --- a/frappe/social/doctype/energy_point_rule/energy_point_rule.py +++ b/frappe/social/doctype/energy_point_rule/energy_point_rule.py @@ -57,11 +57,7 @@ class EnergyPointRule(Document): self.apply_only_once, ) except Exception as e: -<<<<<<< HEAD - frappe.log_error(frappe.get_traceback(), "apply_energy_point") -======= - self.log_error('Energy points failed') ->>>>>>> 6d82805831 (feat(minor): Add document reference to Error Log and doc.log_error) + self.log_error("Energy points failed") def rule_condition_satisfied(self, doc): if self.for_doc_event == "New": From fca6c3d305fc64458c070eedf1ea49e44dc50e96 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 18 Apr 2022 15:59:38 +0530 Subject: [PATCH 10/57] fix(minor): circular imports? --- frappe/cache_manager.py | 1 - frappe/model/naming.py | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index b15f8f2234..01ccc03753 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -5,7 +5,6 @@ import json import frappe from frappe.desk.notifications import clear_notifications, delete_notification_count_for -from frappe.model.document import Document common_default_keys = ["__default", "__global"] diff --git a/frappe/model/naming.py b/frappe/model/naming.py index 9d1079d995..aa502f5a4c 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -6,7 +6,6 @@ from typing import TYPE_CHECKING, Optional, Union import frappe from frappe import _ -from frappe.database.sequence import get_next_val, set_next_val from frappe.model import log_types from frappe.query_builder import DocType from frappe.utils import cint, cstr, now_datetime @@ -36,6 +35,8 @@ def set_new_name(doc): doc.name = None if is_autoincremented(doc.doctype, meta): + from frappe.database.sequence import get_next_val + doc.name = get_next_val(doc.doctype) return @@ -322,11 +323,14 @@ def get_default_naming_series(doctype): def validate_name(doctype: str, name: Union[int, str], case: Optional[str] = None): + if not name: frappe.throw(_("No Name Specified for {0}").format(doctype)) if isinstance(name, int): if is_autoincremented(doctype): + from frappe.database.sequence import set_next_val + # this will set the sequence val to be the provided name and set it to be used # so that the sequence will start from the next val of the setted val(name) set_next_val(doctype, name, is_val_used=True) From 6e6fe9521e403aadb73df5e0b20eb683b3f30617 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 18 Apr 2022 16:14:24 +0530 Subject: [PATCH 11/57] fix(linting): no single quotes :'| --- frappe/__init__.py | 23 +++++++++++-------- .../doctype/auto_repeat/auto_repeat.py | 2 +- .../doctype/communication/communication.py | 2 +- frappe/core/doctype/communication/email.py | 2 +- .../core/doctype/data_import/data_import.py | 2 +- .../prepared_report/prepared_report.py | 2 +- frappe/core/doctype/user/user.py | 2 +- frappe/desk/desktop.py | 6 +++-- .../auto_email_report/auto_email_report.py | 5 +++- .../email/doctype/email_queue/email_queue.py | 10 ++++---- .../doctype/notification/notification.py | 8 +++---- .../doctype/event_consumer/event_consumer.py | 2 +- frappe/model/document.py | 8 +++---- frappe/model/workflow.py | 6 ++--- frappe/utils/print_format.py | 9 ++++---- 15 files changed, 49 insertions(+), 40 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 07a580944f..dcc401c036 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -2069,7 +2069,7 @@ def logger( def log_error(title=None, message=None, reference_doctype=None, reference_name=None): - '''Log error to Error Log''' + """Log error to Error Log""" # Parameter ALERT: # the title and message may be swapped @@ -2077,7 +2077,7 @@ def log_error(title=None, message=None, reference_doctype=None, reference_name=N # this hack tries to be smart about whats a title (single line ;-)) and fixes it if message: - if '\n' in title: # traceback sent as title + if "\n" in title: # traceback sent as title traceback, title = title, message else: traceback = message @@ -2086,15 +2086,18 @@ def log_error(title=None, message=None, reference_doctype=None, reference_name=N traceback = get_traceback() if not title: - title = 'Error' + title = "Error" + + return get_doc( + dict( + doctype="Error Log", + error=as_unicode(traceback), + method=title, + reference_doctype=reference_doctype, + reference_name=reference_name, + ) + ).insert(ignore_permissions=True) - return get_doc(dict( - doctype='Error Log', - error=as_unicode(traceback), - method=title, - reference_doctype=reference_doctype, - reference_name=reference_name - )).insert(ignore_permissions=True) def get_desk_link(doctype, name): html = ( diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 4b2f83f4b6..d3399f7726 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -189,7 +189,7 @@ class AutoRepeat(Document): if self.notify_by_email and self.recipients: self.send_notification(new_doc) except Exception: - error_log = self.log_error('Auto repeat failed') + error_log = self.log_error("Auto repeat failed") self.disable_auto_repeat() diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index eeddb7c7a1..f0e80c2207 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -450,7 +450,7 @@ def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[st contact.insert(ignore_permissions=True) contact_name = contact.name except Exception: - contact.log_error('Unable to add contact') + contact.log_error("Unable to add contact") if contact_name: contacts.append(contact_name) diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index d47b15d360..464bc35a1c 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -248,7 +248,7 @@ def mark_email_as_seen(name: str = None): frappe.db.commit() # nosemgrep: this will be called in a GET request except Exception: - frappe.log_error('Unable to mark as seen', None, 'Communication', name) + frappe.log_error("Unable to mark as seen", None, "Communication", name) finally: frappe.response.update( diff --git a/frappe/core/doctype/data_import/data_import.py b/frappe/core/doctype/data_import/data_import.py index 3693ea53a4..06282e5831 100644 --- a/frappe/core/doctype/data_import/data_import.py +++ b/frappe/core/doctype/data_import/data_import.py @@ -113,7 +113,7 @@ def start_import(data_import): except Exception: frappe.db.rollback() data_import.db_set("status", "Error") - data_import.log_error('Data import failed') + data_import.log_error("Data import failed") finally: frappe.flags.in_import = False diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py index dbdba19fbe..e35ec43565 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.py +++ b/frappe/core/doctype/prepared_report/prepared_report.py @@ -47,7 +47,7 @@ def run_background(prepared_report): instance.save(ignore_permissions=True) except Exception: - report.log_error('Prepared report failed') + report.log_error("Prepared report failed") instance = frappe.get_doc("Prepared Report", prepared_report) instance.status = "Error" instance.error_message = frappe.get_traceback() diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index ec7374d72a..e81f5ecd99 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -265,7 +265,7 @@ class User(Document): except frappe.OutgoingEmailError: # email server not set, don't send email - self.log_error('Unable to send new password notification') + self.log_error("Unable to send new password notification") @Document.hook def validate_reset_password(self): diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index ae34cf3df2..4c82fe8c73 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -338,7 +338,7 @@ def get_desktop_page(page): "onboardings": workspace.onboardings, } except DoesNotExistError: - frappe.log_error('Workspace Missing') + frappe.log_error("Workspace Missing") return {} @@ -469,7 +469,9 @@ def save_new_widget(doc, page, blocks, new_widgets): page: {0} config: {1} exception: {2} - """.format(page, json_config, e) + """.format( + page, json_config, e + ) doc.log_error("Could not save customization", log) return False diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py index 108cc46792..7a9af6149a 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.py +++ b/frappe/email/doctype/auto_email_report/auto_email_report.py @@ -255,7 +255,10 @@ def send_daily(): try: auto_email_report.send() except Exception as e: - auto_email_report.log_error('Failed to send {0} Auto Email Report'.format(auto_email_report.name)) + auto_email_report.log_error( + "Failed to send {0} Auto Email Report".format(auto_email_report.name) + ) + def send_monthly(): """Check reports to be sent monthly""" diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py index b3e52ef1e2..61d730829b 100644 --- a/frappe/email/doctype/email_queue/email_queue.py +++ b/frappe/email/doctype/email_queue/email_queue.py @@ -198,7 +198,7 @@ class SendMailContext: traceback_string = "".join(traceback.format_tb(exc_tb)) traceback_string += f"\n Queue Name: {self.queue_doc.name}" - self.queue_doc.log_error('Email sending failed', traceback_string) + self.queue_doc.log_error("Email sending failed", traceback_string) @property def smtp_session(self): @@ -623,9 +623,11 @@ class QueueBuilder: except frappe.InvalidEmailAddressError: # bad Email Address - don't add to queue self.log_error( - title = 'Invalid email address', - message = 'Invalid email address Sender: {0}, Recipients: {1}, \nTraceback: {2} ' - .format(self.sender, ', '.join(self.final_recipients()), traceback.format_exc())) + title="Invalid email address", + message="Invalid email address Sender: {0}, Recipients: {1}, \nTraceback: {2} ".format( + self.sender, ", ".join(self.final_recipients()), traceback.format_exc() + ), + ) return d = { diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index e868c0ceb6..5543ae6b5d 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -141,7 +141,7 @@ def get_context(context): self.create_system_notification(doc, context) except: - self.log_error('Failed to send Notification') + self.log_error("Failed to send Notification") if self.set_property_after_alert: allow_update = True @@ -168,7 +168,7 @@ def get_context(context): doc.save(ignore_permissions=True) doc.flags.in_notification_update = False except Exception: - self.log_error('Document update failed') + self.log_error("Document update failed") def create_system_notification(self, doc, context): subject = self.subject @@ -432,8 +432,8 @@ def evaluate_alert(doc, alert, event): if event == "Value Change" and not doc.is_new(): if not frappe.db.has_column(doc.doctype, alert.value_changed): - alert.db_set('enabled', 0) - alert.log_error('Notification {0} has been disabled due to missing field'.format(alert.name)) + alert.db_set("enabled", 0) + alert.log_error("Notification {0} has been disabled due to missing field".format(alert.name)) return doc_before_save = doc.get_doc_before_save() diff --git a/frappe/event_streaming/doctype/event_consumer/event_consumer.py b/frappe/event_streaming/doctype/event_consumer/event_consumer.py index dd3d2ec74c..bcd3d3be39 100644 --- a/frappe/event_streaming/doctype/event_consumer/event_consumer.py +++ b/frappe/event_streaming/doctype/event_consumer/event_consumer.py @@ -213,5 +213,5 @@ def has_consumer_access(consumer, update_log): else: return frappe.safe_eval(condition, frappe._dict(doc=doc)) except Exception as e: - consumer.log_error('has_consumer_access error') + consumer.log_error("has_consumer_access error") return False diff --git a/frappe/model/document.py b/frappe/model/document.py index a7b59ae749..67e1de0932 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -1363,12 +1363,10 @@ class Document(BaseDocument): frappe.local.flags.commit = True def log_error(self, title=None, message=None): - '''Helper function to create an Error Log''' + """Helper function to create an Error Log""" return frappe.log_error( - message = message, - title = title, - reference_doctype = self.doctype, - reference_name = self.name) + message=message, title=title, reference_doctype=self.doctype, reference_name=self.name + ) def get_signature(self): """Returns signature (hash) for private URL.""" diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py index 092ad4f764..96fd710d91 100644 --- a/frappe/model/workflow.py +++ b/frappe/model/workflow.py @@ -254,9 +254,9 @@ def bulk_workflow_approval(docnames, doctype, action): frappe.db.rollback() frappe.log_error( - title = "Workflow {0} threw an error for {1} {2}".format(action, doctype, docname), - reference_doctype = 'Workflow', - reference_name = action + title="Workflow {0} threw an error for {1} {2}".format(action, doctype, docname), + reference_doctype="Workflow", + reference_name=action, ) finally: if not message_dict: diff --git a/frappe/utils/print_format.py b/frappe/utils/print_format.py index 2488fae0c9..87fb646f47 100644 --- a/frappe/utils/print_format.py +++ b/frappe/utils/print_format.py @@ -93,10 +93,11 @@ def download_multi_pdf(doctype, name, format=None, no_letterhead=False, options= ) except Exception: frappe.log_error( - title = 'Error in Multi PDF download', - message = "Permission Error on doc {} of doctype {}".format(doc_name, doctype_name), - reference_doctype = doctype_name, - reference_name = doc_name) + title="Error in Multi PDF download", + message="Permission Error on doc {} of doctype {}".format(doc_name, doctype_name), + reference_doctype=doctype_name, + reference_name=doc_name, + ) frappe.local.response.filename = "{}.pdf".format(name) frappe.local.response.filecontent = read_multi_pdf(output) From d9fbee3b9c77587960fcce2b1f7817ff2267c465 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 18 Apr 2022 16:19:46 +0530 Subject: [PATCH 12/57] fix(minor): frappe.log_error --- frappe/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/__init__.py b/frappe/__init__.py index dcc401c036..e3a3769480 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -2076,6 +2076,7 @@ def log_error(title=None, message=None, reference_doctype=None, reference_name=N # the better API for this is log_error(title, message), and used in many cases this way # this hack tries to be smart about whats a title (single line ;-)) and fixes it + traceback = None if message: if "\n" in title: # traceback sent as title traceback, title = title, message From 626ef14e1ef03d2f02e17a83c4c04bb1ad1da927 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 18 Apr 2022 16:23:11 +0530 Subject: [PATCH 13/57] fix(linting): fix quotes --- frappe/email/doctype/email_account/email_account.py | 5 +++-- frappe/email/queue.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 65586de31d..73ab13b851 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -667,7 +667,7 @@ class EmailAccount(Document): try: email_server = self.get_incoming_server(in_receive=True) except Exception: - self.log_error(title=_("Error while connecting to email account {0}").format(self.name)) + self.log_error("Email Connection Error") if not email_server: return @@ -679,7 +679,8 @@ class EmailAccount(Document): message = safe_encode(message) email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message) except Exception: - self.log_error(title="EmailAccount.append_email_to_sent_folder") + self.log_error("Unable to add to Sent folder") + @frappe.whitelist() def get_append_to( diff --git a/frappe/email/queue.py b/frappe/email/queue.py index ac873182a7..b92dea3e65 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -170,7 +170,7 @@ def flush(from_test=False): is_background_task = not from_test func(email_queue_name=row.name, is_background_task=is_background_task) except Exception: - frappe.get_doc('Email Queue', row.name).log_error() + frappe.get_doc("Email Queue", row.name).log_error() def get_queue(): From 930ae45fb8669aae8c2f9fb2bd89e08f4fb170c0 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 18 Apr 2022 17:27:33 +0530 Subject: [PATCH 14/57] fix(test): add a test for Error Log --- frappe/core/doctype/error_log/error_log.json | 5 +++-- frappe/core/doctype/error_log/test_error_log.py | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/frappe/core/doctype/error_log/error_log.json b/frappe/core/doctype/error_log/error_log.json index e643bbb090..b2ab516bba 100644 --- a/frappe/core/doctype/error_log/error_log.json +++ b/frappe/core/doctype/error_log/error_log.json @@ -50,7 +50,7 @@ "icon": "fa fa-warning-sign", "idx": 1, "links": [], - "modified": "2022-04-18 14:51:30.604304", + "modified": "2022-04-18 17:25:47.406873", "modified_by": "Administrator", "module": "Core", "name": "Error Log", @@ -71,5 +71,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "ASC", - "states": [] + "states": [], + "title_field": "method" } \ No newline at end of file diff --git a/frappe/core/doctype/error_log/test_error_log.py b/frappe/core/doctype/error_log/test_error_log.py index e20ac92650..a8511b238e 100644 --- a/frappe/core/doctype/error_log/test_error_log.py +++ b/frappe/core/doctype/error_log/test_error_log.py @@ -9,4 +9,8 @@ import frappe class TestErrorLog(unittest.TestCase): - pass + def test_error_log(self): + """let's do an error log on error log?""" + doc = frappe.new_doc("Error Log") + error = doc.log_error("This is an error") + self.assertEqual(error.doctype, "Error Log") From de8f066dce7878bcff56b3aa7bde8b4aaea34189 Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Mon, 18 Apr 2022 17:31:36 +0530 Subject: [PATCH 15/57] fix: Set dependant property of grid fields while rendering (#16548) --- frappe/public/js/frappe/form/grid_row.js | 52 ++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index c12ac23319..4bba8ae7ad 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -595,6 +595,8 @@ export default class GridRow { // to get update df for the row let df = this.docfields.find(field => field.fieldname === col[0].fieldname); + this.set_dependant_property(df); + let colsize = col[1]; let txt = this.doc ? frappe.format(this.doc[df.fieldname], df, null, this.doc) : @@ -633,6 +635,56 @@ export default class GridRow { } } + set_dependant_property(df) { + if (!df.reqd && df.mandatory_depends_on && + this.evaluate_depends_on_value(df.mandatory_depends_on)) { + df.reqd = 1; + } + + if (!df.read_only && df.read_only_depends_on && + this.evaluate_depends_on_value(df.read_only_depends_on)) { + df.read_only = 1; + } + } + + evaluate_depends_on_value(expression) { + let out = null; + let doc = this.doc; + + if (!doc) return; + + let parent = this.frm ? this.frm.doc : this.doc || null; + + if (typeof (expression) === 'boolean') { + out = expression; + + } else if (typeof (expression) === 'function') { + out = expression(doc); + + } else if (expression.substr(0, 5)=='eval:') { + try { + out = frappe.utils.eval(expression.substr(5), { doc, parent }); + if (parent && parent.istable && expression.includes('is_submittable')) { + out = true; + } + } catch (e) { + frappe.throw(__('Invalid "depends_on" expression')); + } + + } else if (expression.substr(0, 3)=='fn:' && this.frm) { + out = this.frm.script_manager.trigger(expression.substr(3), this.doctype, this.docname); + } else { + var value = doc[expression]; + if ($.isArray(value)) { + out = !!value.length; + } else { + out = !!value; + } + } + + return out; + } + show_search_row() { // show or remove search columns based on grid rows this.show_search = this.frm && this.frm.doc && From 95a13853c7407c48afc6150033e9e0b977602b8e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 18 Apr 2022 17:34:44 +0530 Subject: [PATCH 16/57] ci(Mergify): configuration update (#16655) Signed-off-by: Ankush Menat [skip ci] --- .mergify.yml | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/.mergify.yml b/.mergify.yml index 838ce75835..7f4c084e30 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -53,3 +53,43 @@ pull_request_rules: {{ title }} (#{{ number }}) {{ body }} + + - name: backport to develop + conditions: + - label="backport develop" + actions: + backport: + branches: + - develop + assignees: + - "{{ author }}" + + - name: backport to version-13-hotfix + conditions: + - label="backport version-13-hotfix" + actions: + backport: + branches: + - version-13-hotfix + assignees: + - "{{ author }}" + + - name: backport to version-13-pre-release + conditions: + - label="backport version-13-pre-release" + actions: + backport: + branches: + - version-13-pre-release + assignees: + - "{{ author }}" + + - name: backport to version-12-hotfix + conditions: + - label="backport version-12-hotfix" + actions: + backport: + branches: + - version-12-hotfix + assignees: + - "{{ author }}" \ No newline at end of file From 7b19c91c373c3820f7a3c029b1df25ca42b0a913 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 18 Apr 2022 19:44:01 +0530 Subject: [PATCH 17/57] fix: kanban board without title field (#16659) --- frappe/public/js/frappe/list/base_list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index d5ee82acce..f64ae39d1b 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -124,7 +124,7 @@ frappe.views.BaseList = class BaseList { // df is passed const df = fieldname; fieldname = df.fieldname; - doctype = df.parent; + doctype = df.parent || doctype; } if (!this.fields) this.fields = []; From afebaa0a23ef066a83339fe4527929c3d1225a3e Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 14 Apr 2022 13:43:43 +0530 Subject: [PATCH 18/57] test: less flaky date control test --- cypress/integration/control_date.js | 58 +++++++++++++++++------------ cypress/support/commands.js | 14 +++++++ 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/cypress/integration/control_date.js b/cypress/integration/control_date.js index 35c585306c..5093c675cf 100644 --- a/cypress/integration/control_date.js +++ b/cypress/integration/control_date.js @@ -1,23 +1,27 @@ context('Date Control', () => { before(() => { cy.login(); - cy.visit('/app/doctype'); - return cy.window().its('frappe').then(frappe => { - return frappe.xcall('frappe.tests.ui_test_helpers.create_doctype', { - name: 'Test Date Control', - fields: [ - { - "label": "Date", - "fieldname": "date", - "fieldtype": "Date", - "in_list_view": 1 - }, - ] - }); - }); + cy.visit('/app'); }); + + function get_dialog(date_field_options) { + return cy.dialog({ + title: 'Date', + fields: [{ + "label": "Date", + "fieldname": "date", + "fieldtype": "Date", + "in_list_view": 1, + ...date_field_options + }] + }); + } + it('Selecting a date from the datepicker', () => { - cy.new_form('Test Date Control'); + cy.clear_dialogs(); + cy.clear_datepickers(); + + get_dialog().as('dialog'); cy.get_field('date', 'Date').click(); cy.get('.datepicker--nav-title').click(); cy.get('.datepicker--nav-title').click({force: true}); @@ -28,12 +32,16 @@ context('Date Control', () => { cy.get('.datepicker--months > .datepicker--cells > .datepicker--cell[data-month=0]').click(); cy.get('.datepicker--days > .datepicker--cells > .datepicker--cell[data-date=15]').click(); - //Verifying if the selected date is displayed in the date field - cy.get_field('date', 'Date').should('have.value', '01-15-2020'); + // Verify if the selected date is set the date field + cy.window().its('cur_dialog.fields_dict.date.value').should('be.equal', '2020-01-15'); }); it('Checking next and previous button', () => { - cy.get_field('date', 'Date').click(); + cy.clear_dialogs(); + cy.clear_datepickers(); + + get_dialog({ default: '2020-01-15' }).as('dialog'); + cy.get_field('date', 'Date').click(); //Clicking on the next button in the datepicker cy.get('.datepicker--nav-action[data-action=next]').click(); @@ -42,7 +50,7 @@ context('Date Control', () => { cy.get('.datepicker--cell[data-date=15]').click({force: true}); //Verifying if the selected date has been displayed in the date field - cy.get_field('date', 'Date').should('have.value', '02-15-2020'); + cy.window().its('cur_dialog.fields_dict.date.value').should('be.equal', '2020-02-15'); cy.wait(500); cy.get_field('date', 'Date').click(); @@ -53,19 +61,23 @@ context('Date Control', () => { cy.get('.datepicker--cell[data-date=15]').click({force: true}); //Verifying if the selected date has been displayed in the date field - cy.get_field('date', 'Date').should('have.value', '01-15-2020'); + cy.window().its('cur_dialog.fields_dict.date.value').should('be.equal', '2020-01-15'); }); it('Clicking on "Today" button gives todays date', () => { - cy.get_field('date', 'Date').click(); + cy.clear_dialogs(); + cy.clear_datepickers(); + + get_dialog({ default: '2020-01-15' }).as('dialog'); + cy.get_field('date', 'Date').click(); //Clicking on "Today" button cy.get('.datepicker--button').click(); //Picking up the todays date - const todays_date = Cypress.moment().format('MM-DD-YYYY'); + const todays_date = new Date().toJSON().split('T')[0]; //Verifying if clicking on "Today" button matches today's date - cy.get_field('date', 'Date').should('have.value', todays_date); + cy.window().its('cur_dialog.fields_dict.date.value').should('be.equal', todays_date); }); }); \ No newline at end of file diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 4847dbcf12..14ab063b59 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -258,6 +258,20 @@ Cypress.Commands.add('hide_dialog', () => { cy.get('.modal:visible').should('not.exist'); }); +Cypress.Commands.add('clear_dialogs', () => { + cy.window().then((win) => { + win.$('.modal, .modal-backdrop').remove(); + }); + cy.get('.modal').should('not.exist'); +}) + +Cypress.Commands.add('clear_datepickers', () => { + cy.window().then((win) => { + win.$('.datepicker').remove(); + }); + cy.get('.datepicker').should('not.exist'); +}) + Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => { return cy .window() From 9482ac43993d11d12bd1205f88041ef3b59508be Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 14 Apr 2022 13:49:39 +0530 Subject: [PATCH 19/57] style: semicolons --- cypress/support/commands.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 14ab063b59..ec24212f01 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -263,14 +263,14 @@ Cypress.Commands.add('clear_dialogs', () => { win.$('.modal, .modal-backdrop').remove(); }); cy.get('.modal').should('not.exist'); -}) +}); Cypress.Commands.add('clear_datepickers', () => { cy.window().then((win) => { win.$('.datepicker').remove(); }); cy.get('.datepicker').should('not.exist'); -}) +}); Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => { return cy From bb58ee5ee62f2cc8a29969bd60e395d9e9932aa3 Mon Sep 17 00:00:00 2001 From: phot0n Date: Wed, 13 Apr 2022 22:56:20 +0530 Subject: [PATCH 20/57] feat(minor): add ignore_xss_filter to customize form field --- frappe/custom/doctype/customize_form/customize_form.py | 1 + .../customize_form_field/customize_form_field.json | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index b4ccb21167..12ef945288 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -596,6 +596,7 @@ docfield_properties = { "in_preview": "Check", "bold": "Check", "no_copy": "Check", + "ignore_xss_filter": "Check", "hidden": "Check", "collapsible": "Check", "collapsible_depends_on": "Data", diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index cc446e321e..b991726123 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -21,6 +21,7 @@ "in_global_search", "in_preview", "bold", + "ignore_xss_filter", "no_copy", "allow_in_quick_entry", "translatable", @@ -453,13 +454,19 @@ "hidden": 1, "label": "Is System Generated", "read_only": 1 + }, + { + "default": "0", + "fieldname": "ignore_xss_filter", + "fieldtype": "Check", + "label": "Ignore XSS Filter" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-03-31 12:05:11.799654", + "modified": "2022-04-13 22:31:14.162661", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", From 1aa267a51906040911ca14beae54d7a2b13bdd9a Mon Sep 17 00:00:00 2001 From: phot0n Date: Tue, 19 Apr 2022 12:41:33 +0530 Subject: [PATCH 21/57] chore: move the field to permission section * chore: fix type in description of ignore_xss_filter in docfield doctype --- frappe/core/doctype/docfield/docfield.json | 4 ++-- .../doctype/customize_form_field/customize_form_field.json | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index 9e9aaf489b..5c7d06c93a 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -347,7 +347,7 @@ }, { "default": "0", - "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field", + "description": "Don't encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field", "fieldname": "ignore_xss_filter", "fieldtype": "Check", "label": "Ignore XSS Filter" @@ -547,7 +547,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-03-02 17:07:32.117897", + "modified": "2022-04-19 12:27:28.641580", "modified_by": "Administrator", "module": "Core", "name": "DocField", diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index b991726123..a7a8eff950 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -21,7 +21,6 @@ "in_global_search", "in_preview", "bold", - "ignore_xss_filter", "no_copy", "allow_in_quick_entry", "translatable", @@ -47,6 +46,7 @@ "report_hide", "remember_last_selected_value", "hide_border", + "ignore_xss_filter", "property_depends_on_section", "mandatory_depends_on", "column_break_33", @@ -457,6 +457,7 @@ }, { "default": "0", + "description": "Don't encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field", "fieldname": "ignore_xss_filter", "fieldtype": "Check", "label": "Ignore XSS Filter" From 317ff3b9e23ecc27626a8a4ab3b2ce307daf7636 Mon Sep 17 00:00:00 2001 From: phot0n Date: Tue, 19 Apr 2022 13:39:51 +0530 Subject: [PATCH 22/57] chore: remove tag link patch --- frappe/patches.txt | 1 - frappe/patches/v12_0/copy_to_parent_for_tags.py | 7 ------- 2 files changed, 8 deletions(-) delete mode 100644 frappe/patches/v12_0/copy_to_parent_for_tags.py diff --git a/frappe/patches.txt b/frappe/patches.txt index bc2bc22637..845ccee09a 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -106,7 +106,6 @@ frappe.patches.v12_0.set_default_incoming_email_port frappe.patches.v12_0.update_global_search frappe.patches.v12_0.setup_tags frappe.patches.v12_0.update_auto_repeat_status_and_not_submittable -frappe.patches.v12_0.copy_to_parent_for_tags frappe.patches.v12_0.create_notification_settings_for_user frappe.patches.v11_0.make_all_prepared_report_attachments_private #2019-11-26 frappe.patches.v12_0.setup_email_linking diff --git a/frappe/patches/v12_0/copy_to_parent_for_tags.py b/frappe/patches/v12_0/copy_to_parent_for_tags.py deleted file mode 100644 index ae3702a0d5..0000000000 --- a/frappe/patches/v12_0/copy_to_parent_for_tags.py +++ /dev/null @@ -1,7 +0,0 @@ -import frappe - - -def execute(): - - frappe.db.sql("UPDATE `tabTag Link` SET parenttype=document_type") - frappe.db.sql("UPDATE `tabTag Link` SET parent=document_name") From ef6bb79f65e0a1867926ef769ad8de16455adf2d Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Tue, 19 Apr 2022 15:51:03 +0530 Subject: [PATCH 23/57] test: Typing on focused input issue (#16669) --- cypress/integration/control_barcode.js | 2 ++ cypress/integration/kanban.js | 1 + 2 files changed, 3 insertions(+) diff --git a/cypress/integration/control_barcode.js b/cypress/integration/control_barcode.js index 03ab61fac4..85a3182397 100644 --- a/cypress/integration/control_barcode.js +++ b/cypress/integration/control_barcode.js @@ -20,6 +20,7 @@ context('Control Barcode', () => { it('should generate barcode on setting a value', () => { get_dialog_with_barcode().as('dialog'); + cy.focused().blur(); cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox') .type('123456789') .blur(); @@ -36,6 +37,7 @@ context('Control Barcode', () => { it('should reset when input is cleared', () => { get_dialog_with_barcode().as('dialog'); + cy.focused().blur(); cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox') .type('123456789') .blur(); diff --git a/cypress/integration/kanban.js b/cypress/integration/kanban.js index bb9aaf3d8c..c7f3f08336 100644 --- a/cypress/integration/kanban.js +++ b/cypress/integration/kanban.js @@ -10,6 +10,7 @@ context('Kanban Board', () => { cy.get('.page-actions .custom-btn-group button').click(); cy.get('.page-actions .custom-btn-group ul.dropdown-menu li').contains('Kanban').click(); + cy.focused().blur(); cy.fill_field('board_name', 'ToDo Kanban', 'Data'); cy.fill_field('field_name', 'Status', 'Select'); cy.click_modal_primary_button('Save'); From d1978d8e62e01e23aba3aec8eb21bd0b130399d2 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 19 Apr 2022 16:49:08 +0530 Subject: [PATCH 24/57] fix: check dates in frappe realm new Date in Cypress context and new Date in frappe context differs sometimes in CI --- cypress/integration/control_date.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cypress/integration/control_date.js b/cypress/integration/control_date.js index 5093c675cf..6d9f0b9bcc 100644 --- a/cypress/integration/control_date.js +++ b/cypress/integration/control_date.js @@ -68,16 +68,15 @@ context('Date Control', () => { cy.clear_dialogs(); cy.clear_datepickers(); - get_dialog({ default: '2020-01-15' }).as('dialog'); + get_dialog().as('dialog'); cy.get_field('date', 'Date').click(); //Clicking on "Today" button cy.get('.datepicker--button').click(); - //Picking up the todays date - const todays_date = new Date().toJSON().split('T')[0]; - //Verifying if clicking on "Today" button matches today's date - cy.window().its('cur_dialog.fields_dict.date.value').should('be.equal', todays_date); + cy.window().then(win => { + expect(win.cur_dialog.fields_dict.date.value).to.be.equal(win.frappe.datetime.get_today()); + }); }); }); \ No newline at end of file From fc4e2780bd1e82f22d91920d50abbd7f11b8b379 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 19 Apr 2022 16:49:34 +0530 Subject: [PATCH 25/57] chore: pretty logging for dialog command --- cypress/support/commands.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index ec24212f01..026c622e78 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -241,8 +241,20 @@ Cypress.Commands.add('clear_cache', () => { }); Cypress.Commands.add('dialog', opts => { - return cy.window().then(win => { - var d = new win.frappe.ui.Dialog(opts); + return cy.window({ log: false }).its('frappe', { log: false }).then(frappe => { + Cypress.log({ + name: "dialog", + displayName: "dialog", + message: 'frappe.ui.Dialog', + consoleProps: () => { + return { + options: opts, + dialog: d + } + } + }); + + var d = new frappe.ui.Dialog(opts); d.show(); return d; }); From b59619f764a09110e8a8a07dff5d4bc6352a4746 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 19 Apr 2022 17:29:09 +0530 Subject: [PATCH 26/57] chore: Update creds to allow updates on protected branch (#16672) --- .github/workflows/release.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 52fc595c96..a6c1243f64 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,7 @@ jobs: uses: actions/checkout@v2 with: fetch-depth: 0 + persist-credentials: false - name: Setup Node.js v14 uses: actions/setup-node@v2 with: @@ -22,4 +23,10 @@ jobs: - name: Create Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + GIT_AUTHOR_NAME: "Frappe PR Bot" + GIT_AUTHOR_EMAIL: "developers@frappe.io" + GIT_COMMITTER_NAME: "Frappe PR Bot" + GIT_COMMITTER_EMAIL: "developers@frappe.io" run: npx semantic-release \ No newline at end of file From aef0c0f66e4b1997ef946998e6822de7a8ee58cf Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 19 Apr 2022 19:21:25 +0530 Subject: [PATCH 27/57] fix: Handle unable to read commit errors --- frappe/commands/utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index bb1a8bc2d2..499dd61421 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -1024,6 +1024,7 @@ def set_config(context, key, value, global_=False, parse=False, as_dict=False): def get_version(output): """Show the versions of all the installed apps.""" from git import Repo + from git.exc import InvalidGitRepositoryError from frappe.utils.change_log import get_app_branch from frappe.utils.commands import render_table @@ -1034,12 +1035,16 @@ def get_version(output): for app in sorted(frappe.get_all_apps()): module = frappe.get_module(app) app_hooks = frappe.get_module(app + ".hooks") - repo = Repo(frappe.get_app_path(app, "..")) app_info = frappe._dict() + + try: + app_info.commit = Repo(frappe.get_app_path(app, "..")).head.object.hexsha[:7] + except InvalidGitRepositoryError: + app_info.commit = "" + app_info.app = app app_info.branch = get_app_branch(app) - app_info.commit = repo.head.object.hexsha[:7] app_info.version = getattr(app_hooks, f"{app_info.branch}_version", None) or module.__version__ data.append(app_info) From 8b010e1732b1095e9208d36a5c2dcab16855e7e6 Mon Sep 17 00:00:00 2001 From: Pruthvi Patel Date: Tue, 19 Apr 2022 20:17:22 +0530 Subject: [PATCH 28/57] refactor: make `frappe.db.bulk_insert` work as expected (#16527) ## Issue `frappe.db.bulk_insert` is not working as expected: - It will not insert any row if there are less than 3 values - It will not add 1st row at all. e.g if I'm adding 5 values, it will only add 4. - It will add values one by one after 2 values, instead it should have inserted items (in db) in chunk of 10000 (as per the code written before). ## Changes Made - Solved above issues - use better way to chunk list - Added Postgres support for bulk_insert API And now `bulk_insert` will only do **1 db call for each 10000** values. Note: For testing purpose I made `Test Bulk Insert` doctype and keep chunk size of 100. ## Before ![image](https://user-images.githubusercontent.com/43115036/161979365-c1100745-7602-47d2-a9b8-62d797d2039f.png) ## After ![image](https://user-images.githubusercontent.com/43115036/161978344-3c17d56b-2195-40f4-b00c-e9478d4083f1.png) nodocs --- frappe/database/database.py | 29 +++++++++++++---------------- frappe/tests/test_db.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index 424bcbbc63..411888df34 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -1228,7 +1228,7 @@ class Database(object): frappe.flags.touched_tables = set() frappe.flags.touched_tables.update(tables) - def bulk_insert(self, doctype, fields, values, ignore_duplicates=False): + def bulk_insert(self, doctype, fields, values, ignore_duplicates=False, *, chunk_size=10_000): """ Insert multiple records at a time @@ -1236,22 +1236,19 @@ class Database(object): :param fields: list of fields :params values: list of list of values """ - insert_list = [] - fields = ", ".join("`" + field + "`" for field in fields) - for idx, value in enumerate(values): - insert_list.append(tuple(value)) - if idx and (idx % 10000 == 0 or idx < len(values) - 1): - self.sql( - """INSERT {ignore_duplicates} INTO `tab{doctype}` ({fields}) VALUES {values}""".format( - ignore_duplicates="IGNORE" if ignore_duplicates else "", - doctype=doctype, - fields=fields, - values=", ".join(["%s"] * len(insert_list)), - ), - tuple(insert_list), - ) - insert_list = [] + table = frappe.qb.DocType(doctype) + for start_index in range(0, len(values), chunk_size): + query = frappe.qb.into(table) + if ignore_duplicates: + # Pypika does not have same api for ignoring duplicates + if frappe.conf.db_type == "mariadb": + query = query.ignore() + elif frappe.conf.db_type == "postgres": + query = query.on_conflict().do_nothing() + + values_to_insert = values[start_index : start_index + chunk_size] + query.columns(fields).insert(*values_to_insert).run() def enqueue_jobs_after_commit(): diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index f722ad1d65..5b469cd5db 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -4,6 +4,7 @@ import datetime import inspect import unittest +from math import ceil from random import choice from unittest.mock import patch @@ -445,6 +446,33 @@ class TestDB(unittest.TestCase): self.assertEqual(frappe.db.exists(dt, [["name", "=", dn]]), dn) + def test_bulk_insert(self): + current_count = frappe.db.count("ToDo") + test_body = f"test_bulk_insert - {random_string(10)}" + chunk_size = 10 + + for number_of_values in (1, 2, 5, 27): + current_transaction_writes = frappe.db.transaction_writes + + frappe.db.bulk_insert( + "ToDo", + ["name", "description"], + [[f"ToDo Test Bulk Insert {i}", test_body] for i in range(number_of_values)], + ignore_duplicates=True, + chunk_size=chunk_size, + ) + + # check that all records were inserted + self.assertEqual(number_of_values, frappe.db.count("ToDo") - current_count) + + # check if inserts were done in chunks + expected_number_of_writes = ceil(number_of_values / chunk_size) + self.assertEqual( + expected_number_of_writes, frappe.db.transaction_writes - current_transaction_writes + ) + + frappe.db.delete("ToDo", {"description": test_body}) + @run_only_if(db_type_is.MARIADB) class TestDDLCommandsMaria(unittest.TestCase): From 91e0cac029ad3a969ed04dd0d61b699ba84d69a8 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 20 Apr 2022 11:25:38 +0530 Subject: [PATCH 29/57] fix: Check for required services running after frappe init Each site on a single app server can run on separate DBMS' on separate servers. This site specific config resides in each site's site_config.json file. Thereby, we need to load site's config before checking for service's availability. --- frappe/migrate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/migrate.py b/frappe/migrate.py index bb83fa5b6d..1c249dfdb1 100644 --- a/frappe/migrate.py +++ b/frappe/migrate.py @@ -159,13 +159,13 @@ class SiteMigration: """Run Migrate operation on site specified. This method initializes and destroys connections to the site database. """ - if not self.required_services_running(): - raise SystemExit(1) - if site: frappe.init(site=site) frappe.connect() + if not self.required_services_running(): + raise SystemExit(1) + self.setUp() try: self.pre_schema_updates() From 11360c5fd810c83bd61731e8f430d84ec95dfdcf Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Wed, 20 Apr 2022 13:02:42 +0530 Subject: [PATCH 30/57] fix: Check mandatory_depends_on also while checking mandatory on save (#16515) --- frappe/public/js/frappe/form/save.js | 39 +++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/save.js b/frappe/public/js/frappe/form/save.js index 90516b7c0a..f1cb42250a 100644 --- a/frappe/public/js/frappe/form/save.js +++ b/frappe/public/js/frappe/form/save.js @@ -130,7 +130,8 @@ frappe.ui.form.save = function (frm, action, callback, btn) { folded = frm.layout.folded; } - if (df.reqd && !frappe.model.has_value(doc.doctype, doc.name, df.fieldname)) { + if (is_docfield_mandatory(doc, df) && + !frappe.model.has_value(doc.doctype, doc.name, df.fieldname)) { has_errors = true; error_fields[error_fields.length] = __(df.label); // scroll to field @@ -173,6 +174,42 @@ frappe.ui.form.save = function (frm, action, callback, btn) { return !has_errors; }; + let is_docfield_mandatory = function(doc, df) { + if (df.reqd) return true; + if (!df.mandatory_depends_on || !doc) return; + + let out = null; + let expression = df.mandatory_depends_on; + let parent = frappe.get_meta(df.parent); + + if (typeof (expression) === 'boolean') { + out = expression; + + } else if (typeof (expression) === 'function') { + out = expression(doc); + + } else if (expression.substr(0, 5) == 'eval:') { + try { + out = frappe.utils.eval(expression.substr(5), { doc, parent }); + if (parent && parent.istable && expression.includes('is_submittable')) { + out = true; + } + } catch (e) { + frappe.throw(__('Invalid "mandatory_depends_on" expression')); + } + + } else { + var value = doc[expression]; + if ($.isArray(value)) { + out = !!value.length; + } else { + out = !!value; + } + } + + return out; + }; + const scroll_to = (fieldname) => { frm.scroll_to_field(fieldname); frm.scroll_set = true; From dcee40046a0dc65a78b05f54173eb2bb4f014890 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 20 Apr 2022 15:16:04 +0530 Subject: [PATCH 31/57] perf!: simpler frappe.get_system_setting (#16685) Not sure why this needs to be "cached" in locals again when db object already caches it in value_cache. --- frappe/__init__.py | 5 +---- frappe/core/doctype/system_settings/system_settings.py | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 10c8afbf23..cbf62a8d7d 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -217,7 +217,6 @@ def init(site, sites_path=None, new_site=False): local.module_app = None local.app_modules = None - local.system_settings = _dict() local.user = None local.user_perms = None @@ -2139,9 +2138,7 @@ def safe_eval(code, eval_globals=None, eval_locals=None): def get_system_settings(key): - if key not in local.system_settings: - local.system_settings.update({key: db.get_single_value("System Settings", key)}) - return local.system_settings.get(key) + return db.get_single_value("System Settings", key, cache=True) def get_active_domains(): diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py index 3d01015087..e4d36b7fc7 100644 --- a/frappe/core/doctype/system_settings/system_settings.py +++ b/frappe/core/doctype/system_settings/system_settings.py @@ -53,7 +53,6 @@ class SystemSettings(Document): frappe.cache().delete_value("system_settings") frappe.cache().delete_value("time_zone") - frappe.local.system_settings = {} if frappe.flags.update_last_reset_password_date: update_last_reset_password_date() From 248c3555e3537c874b448e7c62b2af7a5a6bc751 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 20 Apr 2022 15:16:44 +0530 Subject: [PATCH 32/57] fix(minor): add error_log for failed webhooks and web pages --- frappe/integrations/doctype/webhook/webhook.py | 2 +- frappe/public/scss/website/my_account.scss | 1 + frappe/website/serve.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index 04a1c6c21e..3f1b60d903 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -105,7 +105,7 @@ def enqueue_webhook(doc, webhook): if i != 2: continue else: - raise e + webhook.log_error("Webhook failed") def log_request(url, headers, data, res): diff --git a/frappe/public/scss/website/my_account.scss b/frappe/public/scss/website/my_account.scss index 58075580a2..82c4164a31 100644 --- a/frappe/public/scss/website/my_account.scss +++ b/frappe/public/scss/website/my_account.scss @@ -30,6 +30,7 @@ .my-account-container { max-width: 800px; margin: auto; + margin-bottom: 4rem; } .account-info { diff --git a/frappe/website/serve.py b/frappe/website/serve.py index b30f3f1047..2c33b5df51 100644 --- a/frappe/website/serve.py +++ b/frappe/website/serve.py @@ -20,6 +20,7 @@ def get_response(path=None, http_status_code=200): except frappe.PermissionError as e: response = NotPermittedPage(endpoint, http_status_code, exception=e).render() except Exception as e: + frappe.log_error(f"{path} failed") response = ErrorPage(exception=e).render() return response From 296a6bd02f203d68b212991deec3702384943af2 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 20 Apr 2022 15:37:54 +0530 Subject: [PATCH 33/57] test: show locals on test failure (#16687) - Show locals always in CI - Show locals locally when running in verbose mode --- frappe/parallel_test_runner.py | 1 + frappe/test_runner.py | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/frappe/parallel_test_runner.py b/frappe/parallel_test_runner.py index f5367e9dc6..65c8eb470b 100644 --- a/frappe/parallel_test_runner.py +++ b/frappe/parallel_test_runner.py @@ -117,6 +117,7 @@ class ParallelTestRunner: class ParallelTestResult(unittest.TextTestResult): def startTest(self, test): + self.tb_locals = True self._started_at = time.time() super(unittest.TextTestResult, self).startTest(test) test_class = unittest.util.strclass(test.__class__) diff --git a/frappe/test_runner.py b/frappe/test_runner.py index 5fa9f60197..509be36f86 100644 --- a/frappe/test_runner.py +++ b/frappe/test_runner.py @@ -15,6 +15,7 @@ import frappe import frappe.utils.scheduler from frappe.model.naming import revert_series_if_last from frappe.modules import get_module_name, load_doctype_module +from frappe.utils import cint unittest_runner = unittest.TextTestRunner SLOW_TEST_THRESHOLD = 2 @@ -177,10 +178,13 @@ def run_all_tests( _add_test(app, path, filename, verbose, test_suite, ui_tests) if junit_xml_output: - runner = unittest_runner(verbosity=1 + (verbose and 1 or 0), failfast=failfast) + runner = unittest_runner(verbosity=1 + cint(verbose), failfast=failfast) else: runner = unittest_runner( - resultclass=TimeLoggingTestResult, verbosity=1 + (verbose and 1 or 0), failfast=failfast + resultclass=TimeLoggingTestResult, + verbosity=1 + cint(verbose), + failfast=failfast, + tb_locals=verbose, ) if profile: @@ -279,10 +283,13 @@ def _run_unittest( test_suite.addTest(module_test_cases) if junit_xml_output: - runner = unittest_runner(verbosity=1 + (verbose and 1 or 0), failfast=failfast) + runner = unittest_runner(verbosity=1 + cint(verbose), failfast=failfast) else: runner = unittest_runner( - resultclass=TimeLoggingTestResult, verbosity=1 + (verbose and 1 or 0), failfast=failfast + resultclass=TimeLoggingTestResult, + verbosity=1 + cint(verbose), + failfast=failfast, + tb_locals=verbose, ) if profile: From 35c606707ec115b76f4ecfe602aff65dfd33164a Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 20 Apr 2022 16:43:19 +0530 Subject: [PATCH 34/57] fix(bulk_insert): Cast values as list before subscripting --- frappe/database/database.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index 411888df34..d4677a1295 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -1236,8 +1236,9 @@ class Database(object): :param fields: list of fields :params values: list of list of values """ - + values = list(values) table = frappe.qb.DocType(doctype) + for start_index in range(0, len(values), chunk_size): query = frappe.qb.into(table) if ignore_duplicates: From 56bb11dc17ed16de577263908e51ea6d076ea44f Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 21 Apr 2022 08:49:48 +0530 Subject: [PATCH 35/57] fix: Do not change direction of text if already in RTL mode --- frappe/public/scss/common/quill.scss | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frappe/public/scss/common/quill.scss b/frappe/public/scss/common/quill.scss index 3d5ce61c15..9aab2e9fcd 100644 --- a/frappe/public/scss/common/quill.scss +++ b/frappe/public/scss/common/quill.scss @@ -7,6 +7,7 @@ font-family: inherit; } +/*rtl:begin:ignore*/ .ql-editor { font-family: var(--font-stack); color: var(--text-color); @@ -22,7 +23,15 @@ a[href] { text-decoration: underline; } + .ql-direction-rtl { + direction: rtl; + + .table { + direction: ltr; + } + } } +/*rtl:end:ignore*/ + .ql-toolbar.ql-snow { border-top-left-radius: var(--border-radius); @@ -70,6 +79,7 @@ min-height: 0; max-height: none; overflow: hidden; + resize: none; } } From b5c3ff16ee9dd19bbedb9a8f8d561e82130f8f4e Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 21 Apr 2022 12:43:46 +0530 Subject: [PATCH 36/57] fix: Do not use `set_value` for default to avoid change event --- frappe/public/js/frappe/ui/field_group.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/frappe/public/js/frappe/ui/field_group.js b/frappe/public/js/frappe/ui/field_group.js index eb3dcc4f89..178d1a65cb 100644 --- a/frappe/public/js/frappe/ui/field_group.js +++ b/frappe/public/js/frappe/ui/field_group.js @@ -22,15 +22,17 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { super.make(); this.refresh(); // set default - $.each(this.fields_list, (_, field) => { - if (!is_null(field.df.default)) { - let def_value = field.df.default; + $.each(this.fields_list, function(i, field) { + if (field.df["default"]) { + let def_value = field.df["default"]; - if (def_value === "Today" && field.df.fieldtype === "Date") { + if (def_value == 'Today' && field.df["fieldtype"] == 'Date') { def_value = frappe.datetime.get_today(); } - this.set_value(field.df.fieldname, def_value); + field.set_input(def_value); + // if default and has depends_on, render its fields. + me.refresh_dependency(); } }) @@ -127,7 +129,6 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { if (f) { f.set_value(val).then(() => { f.set_input(val); - f.refresh(); this.refresh_dependency(); resolve(); }); From 4e533682ba2ee3d822016ded2222bcbe4453a70a Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 21 Apr 2022 13:26:12 +0530 Subject: [PATCH 37/57] feat: get_traceback with context --- frappe/__init__.py | 4 ++-- frappe/utils/__init__.py | 14 ++++++++++---- requirements.txt | 1 + 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 97e605394b..257d02a09a 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -354,11 +354,11 @@ def cache() -> "RedisWrapper": return redis_server -def get_traceback(): +def get_traceback(with_context=False): """Returns error traceback.""" from frappe.utils import get_traceback - return get_traceback() + return get_traceback(with_context=with_context) def errprint(msg): diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 3e62589664..d0dfe44760 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -18,6 +18,7 @@ from typing import Generator, Iterable from urllib.parse import quote, urlparse from redis.exceptions import ConnectionError +from traceback_with_variables import iter_exc_lines from werkzeug.test import Client import frappe @@ -255,7 +256,7 @@ def get_gravatar(email): return gravatar_url -def get_traceback() -> str: +def get_traceback(with_context=False) -> str: """ Returns the traceback of the Exception """ @@ -264,10 +265,15 @@ def get_traceback() -> str: if not any([exc_type, exc_value, exc_tb]): return "" - trace_list = traceback.format_exception(exc_type, exc_value, exc_tb) - bench_path = get_bench_path() + "/" + if with_context: + trace_list = iter_exc_lines() + tb = "\n".join(trace_list) + else: + trace_list = traceback.format_exception(exc_type, exc_value, exc_tb) + tb = "".join(cstr(t) for t in trace_list) - return "".join(cstr(t) for t in trace_list).replace(bench_path, "") + bench_path = get_bench_path() + "/" + return tb.replace(bench_path, "") def log(event, details): diff --git a/requirements.txt b/requirements.txt index c77ab1d424..495574e05b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -63,6 +63,7 @@ semantic-version~=2.8.5 sqlparse~=0.4.1 stripe~=2.56.0 terminaltables~=3.1.0 +traceback-with-variables~=2.0.4 urllib3~=1.26.4 Werkzeug~=2.0.3 Whoosh~=2.7.4 From 5ecc9fe4ffec55a70fde9f4196ce74c15630f758 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 21 Apr 2022 13:27:01 +0530 Subject: [PATCH 38/57] refactor(log_error): de-clutter & log context with traceback --- frappe/__init__.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 257d02a09a..4653845c3e 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -2069,7 +2069,6 @@ def logger( def log_error(title=None, message=None, reference_doctype=None, reference_name=None): """Log error to Error Log""" - # Parameter ALERT: # the title and message may be swapped # the better API for this is log_error(title, message), and used in many cases this way @@ -2082,20 +2081,15 @@ def log_error(title=None, message=None, reference_doctype=None, reference_name=N else: traceback = message - if not traceback: - traceback = get_traceback() - - if not title: - title = "Error" + title = title or "Error" + traceback = as_unicode(traceback or get_traceback(with_context=True)) return get_doc( - dict( - doctype="Error Log", - error=as_unicode(traceback), - method=title, - reference_doctype=reference_doctype, - reference_name=reference_name, - ) + doctype="Error Log", + error=traceback, + method=title, + reference_doctype=reference_doctype, + reference_name=reference_name, ).insert(ignore_permissions=True) From c691537e61b5d2d7f0151e4b13a7842d083bdbbb Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 21 Apr 2022 13:28:49 +0530 Subject: [PATCH 39/57] chore: Add typing for ease of development --- frappe/database/database.py | 2 +- frappe/model/base_document.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index d4677a1295..edc57ae543 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -1066,7 +1066,7 @@ class Database(object): now_datetime() - relativedelta(minutes=minutes), )[0][0] - def get_db_table_columns(self, table): + def get_db_table_columns(self, table) -> List[str]: """Returns list of column names from given table.""" columns = frappe.cache().hget("table_columns", table) if columns is None: diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 02eb2ab38c..f8d60d0763 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -2,6 +2,7 @@ # License: MIT. See LICENSE import datetime import json +from typing import Dict, List import frappe from frappe import _ @@ -252,7 +253,7 @@ class BaseDocument(object): def get_valid_dict( self, sanitize=True, convert_dates_to_str=False, ignore_nulls=False, ignore_virtual=False - ): + ) -> Dict: d = frappe._dict() for fieldname in self.meta.get_valid_columns(): d[fieldname] = self.get(fieldname) @@ -329,7 +330,7 @@ class BaseDocument(object): if key not in self.__dict__: self.__dict__[key] = None - def get_valid_columns(self): + def get_valid_columns(self) -> List[str]: if self.doctype not in frappe.local.valid_columns: if self.doctype in DOCTYPES_FOR_DOCTYPE: from frappe.model.meta import get_table_columns @@ -342,7 +343,7 @@ class BaseDocument(object): return frappe.local.valid_columns[self.doctype] - def is_new(self): + def is_new(self) -> bool: return self.get("__islocal") @property @@ -359,7 +360,7 @@ class BaseDocument(object): no_default_fields=False, convert_dates_to_str=False, no_child_table_fields=False, - ): + ) -> Dict: doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str) doc["doctype"] = self.doctype From 418dcdd2f445d39da4fa01bce94f19f973d27d50 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 21 Apr 2022 13:32:28 +0530 Subject: [PATCH 40/57] fix!: Use event as a differentiator for frappe.utils.log --- frappe/utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index d0dfe44760..13e6911634 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -277,7 +277,7 @@ def get_traceback(with_context=False) -> str: def log(event, details): - frappe.logger().info(details) + frappe.logger(event).info(details) def dict_to_str(args, sep="&"): From dfef7192da8e99a0285c6a445806f452f69b2365 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 21 Apr 2022 15:27:59 +0530 Subject: [PATCH 41/57] refactor: remove duplicate code from db.get_descendants (#16699) --- frappe/database/database.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index d4677a1295..978667a7e9 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -1146,18 +1146,13 @@ class Database(object): return frappe.db.is_missing_column(e) def get_descendants(self, doctype, name): - """Return descendants of the current record""" - node_location_indexes = self.get_value(doctype, name, ("lft", "rgt")) - if node_location_indexes: - lft, rgt = node_location_indexes - return self.sql_list( - """select name from `tab{doctype}` - where lft > {lft} and rgt < {rgt}""".format( - doctype=doctype, lft=lft, rgt=rgt - ) - ) - else: - # when document does not exist + """Return descendants of the group node in tree""" + from frappe.utils.nestedset import get_descendants_of + + try: + return get_descendants_of(doctype, name, ignore_permissions=True) + except Exception: + # Can only happen if document doesn't exists - kept for backward compatibility return [] def is_missing_table_or_column(self, e): From 9823e51512e3d4870f6bece76c36d69123724d3c Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 21 Apr 2022 16:00:24 +0530 Subject: [PATCH 42/57] feat(safe_exec): Allow new_doc, get_last_doc, rename_doc, delte_doc * rename_doc points to the unwhitelisted method which supports ignore_permissions check * Allowed other safe utils for better DX --- frappe/utils/safe_exec.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index aa6fa8b67f..2e7ef5da21 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -15,6 +15,8 @@ import frappe.utils.data from frappe import _ from frappe.frappeclient import FrappeClient from frappe.handler import execute_cmd +from frappe.model.delete_doc import delete_doc +from frappe.model.rename_doc import rename_doc from frappe.modules import scrub from frappe.utils.background_jobs import enqueue, get_jobs from frappe.website.utils import get_next_link, get_shade, get_toc @@ -110,12 +112,15 @@ def get_safe_globals(): errprint=frappe.errprint, qb=frappe.qb, get_meta=frappe.get_meta, + new_doc=frappe.new_doc, get_doc=frappe.get_doc, + get_last_doc=frappe.get_last_doc, get_cached_doc=frappe.get_cached_doc, get_list=frappe.get_list, get_all=frappe.get_all, get_system_settings=frappe.get_system_settings, - rename_doc=frappe.rename_doc, + rename_doc=rename_doc, + delete_doc=delete_doc, utils=datautils, get_url=frappe.utils.get_url, render_template=frappe.render_template, From e565deb88e07d952ec5f410e82c6fa8ccc826e6e Mon Sep 17 00:00:00 2001 From: KrutikaBhatt <65107474+KrutikaBhatt@users.noreply.github.com> Date: Thu, 21 Apr 2022 16:00:56 +0530 Subject: [PATCH 43/57] fix(UI): Duration Filter overlapping issue (#16599) --- frappe/public/scss/common/controls.scss | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frappe/public/scss/common/controls.scss b/frappe/public/scss/common/controls.scss index fcc924650e..9df89f23e1 100644 --- a/frappe/public/scss/common/controls.scss +++ b/frappe/public/scss/common/controls.scss @@ -343,11 +343,10 @@ textarea.form-control { .duration-picker { position: absolute; z-index: 999; - border-radius: var(--border-radius); box-shadow: var(--shadow-sm); background: var(--popover-bg); - + width: max-content; &:after, &:before { border: solid transparent; @@ -466,4 +465,4 @@ button.data-pill { top: 0; right: 0; cursor: pointer; -} \ No newline at end of file +} From b6683db57e477df7949ad2658e060c495e8cd447 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 21 Apr 2022 16:09:08 +0530 Subject: [PATCH 44/57] refactor: frappe.rename_doc definition Use explicit naming of args, kwargs and don't accept cmd and ignore_permissions explicitly --- frappe/__init__.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 97e605394b..4e85eb3afc 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1210,18 +1210,35 @@ def reload_doc(module, dt=None, dn=None, force=False, reset_permissions=False): @whitelist() -def rename_doc(*args, **kwargs): +def rename_doc( + doctype: str, + old: str, + new: str, + force: bool = False, + merge: bool = False, + *, + ignore_if_exists: bool = False, + show_alert: bool = True, + rebuild_search: bool = True, +) -> str: """ Renames a doc(dt, old) to doc(dt, new) and updates all linked fields of type "Link" Calls `frappe.model.rename_doc.rename_doc` """ - kwargs.pop("ignore_permissions", None) - kwargs.pop("cmd", None) from frappe.model.rename_doc import rename_doc - return rename_doc(*args, **kwargs) + return rename_doc( + doctype=doctype, + old=old, + new=new, + force=force, + merge=merge, + ignore_if_exists=ignore_if_exists, + show_alert=show_alert, + rebuild_search=rebuild_search, + ) def get_module(modulename): From ab1f893e41c91843512c1b92dac548eb4dfe08d2 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 21 Apr 2022 16:46:49 +0530 Subject: [PATCH 45/57] feat: Add get_mapped_doc in safe_exec under frappe --- frappe/utils/safe_exec.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index 2e7ef5da21..fc53243021 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -16,6 +16,7 @@ from frappe import _ from frappe.frappeclient import FrappeClient from frappe.handler import execute_cmd from frappe.model.delete_doc import delete_doc +from frappe.model.mapper import get_mapped_doc from frappe.model.rename_doc import rename_doc from frappe.modules import scrub from frappe.utils.background_jobs import enqueue, get_jobs @@ -114,6 +115,7 @@ def get_safe_globals(): get_meta=frappe.get_meta, new_doc=frappe.new_doc, get_doc=frappe.get_doc, + get_mapped_doc=get_mapped_doc, get_last_doc=frappe.get_last_doc, get_cached_doc=frappe.get_cached_doc, get_list=frappe.get_list, From 7d9695a0d2aafcc0e15c20d06a64f43d3d96ee54 Mon Sep 17 00:00:00 2001 From: chillaranand Date: Fri, 22 Apr 2022 11:10:54 +0530 Subject: [PATCH 46/57] chore: Bump wrapt to 1.14.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c77ab1d424..099e49a0b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -66,7 +66,7 @@ terminaltables~=3.1.0 urllib3~=1.26.4 Werkzeug~=2.0.3 Whoosh~=2.7.4 -wrapt~=1.12.1 +wrapt~=1.14.0 xlrd~=2.0.1 zxcvbn-python~=4.4.24 tenacity~=8.0.1 From a245cb51a24f044ce9e332c7193c16abfa21d325 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 22 Apr 2022 13:32:31 +0530 Subject: [PATCH 47/57] feat: configurable auto email reports limit (#16684) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The previous limit was 3 per user which is way too less, no known reason to restrict this other than hogging of system with too many reports. Bumped default limit to 20. - site config is not easily discoverable or editable, added config in system settings. - Moved auto email report background job form daily queue to `daily_long` queue. closes https://github.com/frappe/frappe/issues/16681 Screenshot 2022-04-20 at 12 33 06 PM `no-docs` (error message is sufficient to explain to user what to do without referring docs) ref: ISS-21-22-10245 ISS-21-22-07742 ISS-20-21-10850 ISS-20-21-10112 and many more times. This is such a stupid validation 🤦 --- .../system_settings/system_settings.json | 16 ++++++++++++++-- .../auto_email_report/auto_email_report.py | 17 +++++++++++------ frappe/hooks.py | 2 +- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 61410fb1a8..0c9b87e618 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -68,6 +68,8 @@ "prepared_report_section", "enable_prepared_report_auto_deletion", "prepared_report_expiry_period", + "column_break_64", + "max_auto_email_report_per_user", "system_updates_section", "disable_system_update_notification" ], @@ -445,7 +447,7 @@ "collapsible": 1, "fieldname": "prepared_report_section", "fieldtype": "Section Break", - "label": "Prepared Report" + "label": "Reports" }, { "default": "Frappe", @@ -485,12 +487,22 @@ "fieldtype": "Select", "label": "First Day of the Week", "options": "Sunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday" + }, + { + "fieldname": "column_break_64", + "fieldtype": "Column Break" + }, + { + "default": "20", + "fieldname": "max_auto_email_report_per_user", + "fieldtype": "Int", + "label": "Max auto email report per user" } ], "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2022-01-04 11:28:34.881192", + "modified": "2022-04-21 09:11:35.218721", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py index 7a9af6149a..9f897a1308 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.py +++ b/frappe/email/doctype/auto_email_report/auto_email_report.py @@ -12,6 +12,7 @@ from frappe.model.document import Document from frappe.model.naming import append_number_if_name_exists from frappe.utils import ( add_to_date, + cint, format_time, get_link_to_form, get_url_to_report, @@ -51,14 +52,18 @@ class AutoEmailReport(Document): self.email_to = "\n".join(valid) def validate_report_count(self): - """check that there are only 3 enabled reports per user""" - count = frappe.db.sql( - "select count(*) from `tabAuto Email Report` where user=%s and enabled=1", self.user - )[0][0] - max_reports_per_user = frappe.local.conf.max_reports_per_user or 3 + count = frappe.db.count("Auto Email Report", {"user": self.user, "enabled": 1}) + + max_reports_per_user = ( + cint(frappe.local.conf.max_reports_per_user) # kept for backward compatibilty + or cint(frappe.db.get_single_value("System Settings", "max_auto_email_report_per_user")) + or 20 + ) if count > max_reports_per_user + (-1 if self.flags.in_insert else 0): - frappe.throw(_("Only {0} emailed reports are allowed per user").format(max_reports_per_user)) + msg = _("Only {0} emailed reports are allowed per user.").format(max_reports_per_user) + msg += " " + _("To allow more reports update limit in System Settings.") + frappe.throw(msg, title=_("Report limit reached")) def validate_report_format(self): """check if user has select correct report format""" diff --git a/frappe/hooks.py b/frappe/hooks.py index d3de3877ba..f7a67dc7ec 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -226,7 +226,6 @@ scheduler_events = { "frappe.sessions.clear_expired_sessions", "frappe.email.doctype.notification.notification.trigger_daily_alerts", "frappe.utils.scheduler.restrict_scheduler_events_if_dormant", - "frappe.email.doctype.auto_email_report.auto_email_report.send_daily", "frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.remove_unverified_record", "frappe.desk.form.document_follow.send_daily_updates", "frappe.social.doctype.energy_point_settings.energy_point_settings.allocate_review_points", @@ -241,6 +240,7 @@ scheduler_events = { "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily", "frappe.utils.change_log.check_for_update", "frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_daily", + "frappe.email.doctype.auto_email_report.auto_email_report.send_daily", "frappe.integrations.doctype.google_drive.google_drive.daily_backup", ], "weekly_long": [ From e1db9bf653eb63b56ba1814fb0f584b1cf2de858 Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Fri, 22 Apr 2022 15:37:00 +0530 Subject: [PATCH 48/57] fix: Grid layout broken for some grids (#16702) --- frappe/public/js/frappe/form/grid_row.js | 36 +++++++++++++++--------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 4bba8ae7ad..a1c3dce91f 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -289,19 +289,23 @@ export default class GridRow { var me = this; if(this.doc && !this.grid.df.in_place_edit) { // remove row - if(!this.open_form_button) { - this.open_form_button = $(` -
- ${frappe.utils.icon('edit', 'xs')} - -
- `) - .appendTo($('
').appendTo(this.row)) - .on('click', function() { - me.toggle_view(); return false; - }); + if (!this.open_form_button) { + this.open_form_button = $('
').appendTo(this.row); - if(this.is_too_small()) { + if (!this.configure_columns) { + this.open_form_button = $(` +
+ ${frappe.utils.icon('edit', 'xs')} + +
+ `) + .appendTo(this.open_form_button) + .on('click', function() { + me.toggle_view(); return false; + }); + } + + if (this.is_too_small()) { // narrow this.open_form_button.css({'margin-right': '-2px'}); } @@ -310,7 +314,9 @@ export default class GridRow { } add_column_configure_button() { - if (this.configure_columns) { + if (this.grid.df.in_place_edit && !this.frm) return; + + if (this.configure_columns && this.frm) { this.configure_columns_button = $(`
${frappe.utils.icon('setting-gear', 'sm', '', 'filter: opacity(0.5)')} @@ -320,6 +326,10 @@ export default class GridRow { .on('click', () => { this.configure_dialog_for_columns_selector(); }); + } else if (this.configure_columns && !this.frm) { + this.configure_columns_button = $(` +
+ `).appendTo(this.row); } } From 5c3b448c09d0b19bb5f2256d6827ccf7656ca6c4 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Fri, 22 Apr 2022 16:59:03 +0530 Subject: [PATCH 49/57] refactor: used list comprehension --- frappe/tests/ui_test_helpers.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frappe/tests/ui_test_helpers.py b/frappe/tests/ui_test_helpers.py index 4cb3e95a6d..494ba8872d 100644 --- a/frappe/tests/ui_test_helpers.py +++ b/frappe/tests/ui_test_helpers.py @@ -137,12 +137,10 @@ def create_contact_records(): @frappe.whitelist() def create_multiple_todo_records(): - values = [] if frappe.db.get_all("ToDo", {"description": "Multiple ToDo 1"}): return - for index in range(1, 1002): - values.append(("100{}".format(index), "Multiple ToDo {}".format(index))) + values = [("100{}".format(i), "Multiple ToDo {}".format(i)) for i in range(1, 1002)] frappe.db.bulk_insert("ToDo", fields=["name", "description"], values=set(values)) From 7f83178556e6e81690b5d7d94345794035daa399 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Fri, 22 Apr 2022 16:59:22 +0530 Subject: [PATCH 50/57] test: flaky markdown editor fix --- cypress/integration/control_markdown_editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/integration/control_markdown_editor.js b/cypress/integration/control_markdown_editor.js index b527d854d4..128add5397 100644 --- a/cypress/integration/control_markdown_editor.js +++ b/cypress/integration/control_markdown_editor.js @@ -16,7 +16,7 @@ context("Control Markdown Editor", () => { cy.click_modal_primary_button("Upload"); cy.get_field("main_section_md", "Markdown Editor").should( "contain", - "![](/files/sample_image.jpg)" + "![](/files/sample_image" ); }); }); From 8c953b4f74070ec534a42e957b8b075acd852a82 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Fri, 22 Apr 2022 16:59:42 +0530 Subject: [PATCH 51/57] test: removing drag drop test(flaky) --- cypress/integration/kanban.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/cypress/integration/kanban.js b/cypress/integration/kanban.js index c7f3f08336..070af10b0d 100644 --- a/cypress/integration/kanban.js +++ b/cypress/integration/kanban.js @@ -72,14 +72,16 @@ context('Kanban Board', () => { }); - it('Drag todo', () => { - cy.intercept({ - method: 'POST', - url: 'api/method/frappe.desk.doctype.kanban_board.kanban_board.update_order_for_single_card' - }).as('drag-completed'); + // it('Drag todo', () => { + // cy.intercept({ + // method: 'POST', + // url: 'api/method/frappe.desk.doctype.kanban_board.kanban_board.update_order_for_single_card' + // }).as('drag-completed'); - cy.get('.kanban-card-body:first').drag('[data-column-value="Closed"] .kanban-cards', {force: true}); + // cy.get('.kanban-card-body') + // .contains('Test Kanban ToDo').first() + // .drag('[data-column-value="Closed"] .kanban-cards', { force: true }); - cy.wait('@drag-completed'); - }); + // cy.wait('@drag-completed'); + // }); }); \ No newline at end of file From 09a00770795e1296beedb253e7941db3a1fda167 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Fri, 22 Apr 2022 17:13:46 +0530 Subject: [PATCH 52/57] style: fixed indentation --- cypress/integration/kanban.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/integration/kanban.js b/cypress/integration/kanban.js index 070af10b0d..50fc41afe3 100644 --- a/cypress/integration/kanban.js +++ b/cypress/integration/kanban.js @@ -82,6 +82,6 @@ context('Kanban Board', () => { // .contains('Test Kanban ToDo').first() // .drag('[data-column-value="Closed"] .kanban-cards', { force: true }); - // cy.wait('@drag-completed'); + // cy.wait('@drag-completed'); // }); }); \ No newline at end of file From d558f16e714f598502e2897bd873202a2d7ea0cd Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Fri, 22 Apr 2022 17:43:20 +0530 Subject: [PATCH 53/57] test: fixed flaky test for form, timeline_email --- cypress/integration/form.js | 2 +- cypress/integration/timeline_email.js | 4 ++-- cypress/support/commands.js | 7 +++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/cypress/integration/form.js b/cypress/integration/form.js index acaff9a191..99a4336bcb 100644 --- a/cypress/integration/form.js +++ b/cypress/integration/form.js @@ -27,7 +27,7 @@ context('Form', () => { cy.clear_filters(); cy.get('.standard-filter-section [data-fieldname="name"] input').type('Test Form Contact 3').blur(); - cy.click_listview_row_item(0); + cy.click_listview_row_item_with_text('Test Form Contact 3'); cy.get('#page-Contact .page-head').findByTitle('Test Form Contact 3').should('exist'); cy.get('.prev-doc').should('be.visible').click(); diff --git a/cypress/integration/timeline_email.js b/cypress/integration/timeline_email.js index 5808bd52ef..f2a239401d 100644 --- a/cypress/integration/timeline_email.js +++ b/cypress/integration/timeline_email.js @@ -16,7 +16,7 @@ context('Timeline Email', () => { 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(); + cy.click_listview_row_item_with_text('Test ToDo'); //Creating a new email cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click(); @@ -47,7 +47,7 @@ context('Timeline Email', () => { it('Deleting attachment and ToDo', () => { cy.visit('/app/todo'); - cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click(); + cy.click_listview_row_item_with_text('Test ToDo'); //Removing the added attachment cy.get('.attachment-row > .data-pill > .remove-btn > .icon').click(); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 026c622e78..7e2f3116ae 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -352,6 +352,13 @@ Cypress.Commands.add('click_listview_row_item', (row_no) => { cy.get('.list-row > .level-left > .list-subject > .level-item > .ellipsis').eq(row_no).click({force: true}); }); +Cypress.Commands.add('click_listview_row_item_with_text', (text) => { + cy.get('.list-row > .level-left > .list-subject > .level-item > .ellipsis') + .contains(text) + .first() + .click({force: true}); +}); + Cypress.Commands.add('click_filter_button', () => { cy.get('.filter-selector > .btn').click(); }); From 042f98bcc5cc5886e0d0e5ce19483fb760b58cf8 Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Fri, 22 Apr 2022 18:00:44 +0530 Subject: [PATCH 54/57] fix: Workspace Miscellaneous fixes (#16578) --- cypress/integration/workspace.js | 113 ++++++++++++++++-- frappe/desk/doctype/workspace/workspace.py | 29 ++++- frappe/public/js/frappe/form/toolbar.js | 2 +- .../js/frappe/views/workspace/blocks/block.js | 2 +- .../js/frappe/views/workspace/blocks/card.js | 2 +- .../js/frappe/views/workspace/blocks/chart.js | 2 +- .../views/workspace/blocks/onboarding.js | 2 +- .../frappe/views/workspace/blocks/shortcut.js | 2 +- .../public/js/frappe/widgets/widget_dialog.js | 45 +++---- 9 files changed, 159 insertions(+), 40 deletions(-) diff --git a/cypress/integration/workspace.js b/cypress/integration/workspace.js index fbff451305..a12d86b3d6 100644 --- a/cypress/integration/workspace.js +++ b/cypress/integration/workspace.js @@ -2,7 +2,6 @@ context('Workspace 2.0', () => { before(() => { cy.visit('/login'); cy.login(); - cy.visit('/app/website'); }); it('Navigate to page from sidebar', () => { @@ -13,6 +12,11 @@ context('Workspace 2.0', () => { }); it('Create Private Page', () => { + cy.intercept({ + method: 'POST', + url: 'api/method/frappe.desk.doctype.workspace.workspace.new_page' + }).as('new_page'); + cy.get('.codex-editor__redactor .ce-block'); cy.get('.custom-actions button[data-label="Create%20Workspace"]').click(); cy.fill_field('title', 'Test Private Page', 'Data'); @@ -27,12 +31,100 @@ context('Workspace 2.0', () => { cy.wait(300); cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('have.attr', 'item-public', '0'); - cy.wait(500); + cy.wait('@new_page'); + }); + + it('Create Child Page', () => { + cy.intercept({ + method: 'POST', + url: 'api/method/frappe.desk.doctype.workspace.workspace.new_page' + }).as('new_page'); + + cy.get('.codex-editor__redactor .ce-block'); + cy.get('.custom-actions button[data-label="Create%20Workspace"]').click(); + cy.fill_field('title', 'Test Child Page', 'Data'); + cy.fill_field('parent', 'Test Private Page', 'Select'); + cy.fill_field('icon', 'edit', 'Icon'); + cy.get_open_dialog().find('.modal-header').click(); + cy.get_open_dialog().find('.btn-primary').click(); + + // check if sidebar item is added in pubic section + cy.get('.sidebar-item-container[item-name="Test Child Page"]').should('have.attr', 'item-public', '0'); + + cy.get('.standard-actions .btn-primary[data-label="Save"]').click(); + cy.wait(300); + cy.get('.sidebar-item-container[item-name="Test Child Page"]').should('have.attr', 'item-public', '0'); + + cy.wait('@new_page'); + }); + + it('Duplicate Page', () => { + cy.intercept({ + method: 'POST', + url: 'api/method/frappe.desk.doctype.workspace.workspace.duplicate_page' + }).as('page_duplicated'); + 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"]').as('sidebar-item'); + + cy.get('@sidebar-item').find('.standard-sidebar-item').first().click(); + cy.get('@sidebar-item').find('.dropdown-btn').first().click(); + cy.get('@sidebar-item').find('.dropdown-list .dropdown-item').contains('Duplicate').first().click({force: true}); + + cy.get_open_dialog().fill_field('title', 'Duplicate Page', 'Data'); + cy.click_modal_primary_button('Duplicate'); + + cy.wait('@page_duplicated'); + }); + + it('Drag Sidebar Item', () => { + cy.intercept({ + method: 'POST', + url: 'api/method/frappe.desk.doctype.workspace.workspace.sort_pages' + }).as('page_sorted'); + + cy.get('.sidebar-item-container[item-name="Duplicate Page"]').as('sidebar-item'); + + cy.get('@sidebar-item').find('.standard-sidebar-item').first().click(); + cy.get('@sidebar-item').find('.drag-handle').first().move({ deltaX: 0, deltaY: 100 }); + + cy.get('.sidebar-item-container[item-name="Build"]').as('sidebar-item'); + + cy.get('@sidebar-item').find('.standard-sidebar-item').first().click(); + cy.get('@sidebar-item').find('.drag-handle').first().move({ deltaX: 0, deltaY: 100 }); + + cy.wait('@page_sorted'); + }); + + it('Edit Page Detail', () => { + cy.intercept({ + method: 'POST', + url: 'api/method/frappe.desk.doctype.workspace.workspace.update_page' + }).as('page_updated'); + + cy.get('.sidebar-item-container[item-name="Test Private Page"]').as('sidebar-item'); + + cy.get('@sidebar-item').find('.standard-sidebar-item').first().click(); + cy.get('@sidebar-item').find('.dropdown-btn').first().click(); + cy.get('@sidebar-item').find('.dropdown-list .dropdown-item').contains('Edit').first().click({force: true}); + + cy.get_open_dialog().fill_field('title', ' 1', 'Data'); + cy.get_open_dialog().find('input[data-fieldname="is_public"]').check(); + cy.click_modal_primary_button('Update'); + + cy.get('.standard-sidebar-section:first .sidebar-item-container[item-name="Test Private Page"]').should('not.exist'); + cy.get('.standard-sidebar-section:last .sidebar-item-container[item-name="Test Private Page 1"]').should('exist'); + + cy.wait('@page_updated'); }); it('Add New Block', () => { + cy.get('.sidebar-item-container[item-name="Duplicate Page"]').as('sidebar-item'); + + cy.get('@sidebar-item').find('.standard-sidebar-item').first().click(); + cy.get('.ce-block').click().type('{enter}'); cy.get('.block-list-container .block-list-item').contains('Heading').click(); cy.get(":focus").type('Header'); @@ -70,19 +162,24 @@ context('Workspace 2.0', () => { cy.get('.standard-actions .btn-primary[data-label="Save"]').click(); }); - it('Delete Private Page', () => { + it('Delete Duplicate Page', () => { + cy.intercept({ + method: 'POST', + url: 'api/method/frappe.desk.doctype.workspace.workspace.delete_page' + }).as('page_deleted'); + 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"]') + cy.get('.sidebar-item-container[item-name="Duplicate Page"]') .find('.sidebar-item-control .setting-btn').click(); - cy.get('.sidebar-item-container[item-name="Test Private Page"]') + cy.get('.sidebar-item-container[item-name="Duplicate 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"]').click(); - cy.get('.codex-editor__redactor .ce-block'); - cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('not.exist'); + cy.get('.sidebar-item-container[item-name="Duplicate Page"]').should('not.exist'); + + cy.wait('@page_deleted'); }); }); \ No newline at end of file diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index a2dbcbfbe2..284fecbe31 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -80,7 +80,14 @@ class Workspace(Document): # remove duplicate before adding for idx, link in enumerate(self.links): - if link.label == card.get("label") and link.type == "Card Break": + if link.get("label") == card.get("label") and link.get("type") == "Card Break": + # count and set number of links for the card if link_count is 0 + if link.link_count == 0: + for count, card_link in enumerate(self.links[idx + 1 :]): + if card_link.get("type") == "Card Break": + break + link.link_count = count + 1 + del self.links[idx : idx + link.link_count + 1] self.append( @@ -199,21 +206,31 @@ def update_page(name, title, icon, parent, 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.label = new_name = "{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) + if name != new_name: + rename_doc("Workspace", name, new_name, 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 + if child_doc.public != public: + child_doc.public = public + child_doc.for_user = "" if public else child_doc.for_user or frappe.session.user + child_doc.label = new_child_name = ( + "{0}-{1}".format(child_doc.title, child_doc.for_user) + if child_doc.for_user + else child_doc.title + ) child_doc.save(ignore_permissions=True) - return {"name": doc.title, "public": doc.public, "label": doc.label} + if child.name != new_child_name: + rename_doc("Workspace", child.name, new_child_name, force=True, ignore_permissions=True) + + return {"name": title, "public": public, "label": new_name} @frappe.whitelist() diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index e55eb9fdeb..6841640341 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -323,7 +323,7 @@ frappe.ui.form.Toolbar = class Toolbar { } // New - if(p[CREATE] && !this.frm.meta.issingle) { + if (p[CREATE] && !this.frm.meta.issingle && !this.frm.meta.in_create) { this.page.add_menu_item(__("New {0}", [__(me.frm.doctype)]), function() { frappe.new_doc(me.frm.doctype, true); }, true, { diff --git a/frappe/public/js/frappe/views/workspace/blocks/block.js b/frappe/public/js/frappe/views/workspace/blocks/block.js index 9605d30c81..1df6b707fe 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/block.js +++ b/frappe/public/js/frappe/views/workspace/blocks/block.js @@ -7,7 +7,7 @@ export default class Block { make(block, block_name, widget_type = block) { let block_data = this.config.page_data[block+'s'].items.find(obj => { - return frappe.utils.unescape_html(obj.label) == frappe.utils.unescape_html(block_name); + return frappe.utils.unescape_html(obj.label) == frappe.utils.unescape_html(__(block_name)); }); if (!block_data) return false; this.wrapper.innerHTML = ''; diff --git a/frappe/public/js/frappe/views/workspace/blocks/card.js b/frappe/public/js/frappe/views/workspace/blocks/card.js index 9ce6ce8b4d..4b46b12890 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/card.js +++ b/frappe/public/js/frappe/views/workspace/blocks/card.js @@ -31,7 +31,7 @@ export default class Card extends Block { this.new('card', 'links'); if (this.data && this.data.card_name) { - let has_data = this.make('card', __(this.data.card_name), 'links'); + let has_data = this.make('card', this.data.card_name, 'links'); if (!has_data) return; } diff --git a/frappe/public/js/frappe/views/workspace/blocks/chart.js b/frappe/public/js/frappe/views/workspace/blocks/chart.js index ccef1fa15f..cb688f48ed 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/chart.js +++ b/frappe/public/js/frappe/views/workspace/blocks/chart.js @@ -32,7 +32,7 @@ export default class Chart extends Block { this.new('chart'); if (this.data && this.data.chart_name) { - let has_data = this.make('chart', __(this.data.chart_name)); + let has_data = this.make('chart', this.data.chart_name); if (!has_data) return; } diff --git a/frappe/public/js/frappe/views/workspace/blocks/onboarding.js b/frappe/public/js/frappe/views/workspace/blocks/onboarding.js index c0ba529853..c76141996f 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/onboarding.js +++ b/frappe/public/js/frappe/views/workspace/blocks/onboarding.js @@ -73,7 +73,7 @@ export default class Onboarding extends Block { make(block, block_name) { let block_data = this.config.page_data['onboardings'].items.find(obj => { - return obj.label == block_name; + return obj.label == __(block_name); }); if (!block_data) return false; this.wrapper.innerHTML = ''; diff --git a/frappe/public/js/frappe/views/workspace/blocks/shortcut.js b/frappe/public/js/frappe/views/workspace/blocks/shortcut.js index 2be5da0d4b..ef9bfa8cf9 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/shortcut.js +++ b/frappe/public/js/frappe/views/workspace/blocks/shortcut.js @@ -51,7 +51,7 @@ export default class Shortcut extends Block { this.new('shortcut'); if (this.data && this.data.shortcut_name) { - let has_data = this.make('shortcut', __(this.data.shortcut_name)); + let has_data = this.make('shortcut', this.data.shortcut_name); if (!has_data) return; } diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js index d1ba75227b..bba29ffaf9 100644 --- a/frappe/public/js/frappe/widgets/widget_dialog.js +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -228,30 +228,35 @@ class CardDialog extends WidgetDialog { } process_data(data) { - data.links.map((item, idx) => { - let message = ''; - let row = idx+1; + let message = ''; - if (!item.link_type) { - message = "Following fields have missing values:

    "; - message += `
  • Link Type in Row ${row}
  • `; - } + if (!data.links) { + message = "You must add atleast one link."; + } else { + data.links.map((item, idx) => { + let row = idx+1; - if (!item.link_to) { - message += `
  • Link To in Row ${row}
  • `; - } + if (!item.link_type) { + message = "Following fields have missing values:

      "; + message += `
    • Link Type in Row ${row}
    • `; + } - if (message) { - message += "
    "; - frappe.throw({ - message: __(message), - title: __("Missing Values Required"), - indicator: 'orange' - }); - } + if (!item.link_to) { + message += `
  • Link To in Row ${row}
  • `; + } - item.label = item.label ? item.label : item.link_to; - }); + item.label = item.label ? item.label : item.link_to; + }); + } + + if (message) { + message += "
"; + frappe.throw({ + message: __(message), + title: __("Missing Values Required"), + indicator: 'orange' + }); + } data.label = data.label ? data.label : data.chart_name; return data; From fa7ea4bce8604f3835f0831a9e952ae66832bf73 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sat, 23 Apr 2022 09:01:20 +0530 Subject: [PATCH 55/57] perf(BaseDocument): ~50% faster `as_dict` (#16549) Co-authored-by: Pruthvi Patel --- frappe/model/__init__.py | 3 ++ frappe/model/base_document.py | 75 +++++++++++++++++------------------ 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index bd607e7119..570df4dab8 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -40,6 +40,9 @@ data_fieldtypes = ( "JSON", ) +float_like_fields = {"Float", "Currency", "Percent"} +datetime_fields = {"Datetime", "Date", "Time"} + attachment_fieldtypes = ( "Attach", "Attach Image", diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index f8d60d0763..93446fb99e 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -6,7 +6,14 @@ from typing import Dict, List import frappe from frappe import _ -from frappe.model import child_table_fields, default_fields, display_fieldtypes, table_fields +from frappe.model import ( + child_table_fields, + datetime_fields, + default_fields, + display_fieldtypes, + float_like_fields, + table_fields, +) from frappe.model.docstatus import DocStatus from frappe.model.naming import set_new_name from frappe.model.utils.link_count import notify_link_count @@ -256,7 +263,8 @@ class BaseDocument(object): ) -> Dict: d = frappe._dict() for fieldname in self.meta.get_valid_columns(): - d[fieldname] = self.get(fieldname) + # column is valid, we can use getattr + d[fieldname] = getattr(self, fieldname, None) # if no need for sanitization and value is None, continue if not sanitize and d[fieldname] is None: @@ -264,25 +272,24 @@ class BaseDocument(object): df = self.meta.get_field(fieldname) - if df and df.get("is_virtual"): - if ignore_virtual: - del d[fieldname] - continue + if df: + if getattr(df, "is_virtual", False): + if ignore_virtual: + del d[fieldname] + continue - from frappe.utils.safe_exec import get_safe_globals + if d[fieldname] is None and (options := getattr(df, "options", None)): + from frappe.utils.safe_exec import get_safe_globals - if d[fieldname] is None: - if df.get("options"): d[fieldname] = frappe.safe_eval( - code=df.get("options"), + code=options, eval_globals=get_safe_globals(), eval_locals={"doc": self}, ) - else: - _val = getattr(self, fieldname, None) - if _val and not callable(_val): - d[fieldname] = _val - elif df: + + if isinstance(d[fieldname], list) and df.fieldtype not in table_fields: + frappe.throw(_("Value for {0} cannot be a list").format(_(df.label))) + if df.fieldtype == "Check": d[fieldname] = 1 if cint(d[fieldname]) else 0 @@ -292,25 +299,20 @@ class BaseDocument(object): elif df.fieldtype == "JSON" and isinstance(d[fieldname], dict): d[fieldname] = json.dumps(d[fieldname], sort_keys=True, indent=4, separators=(",", ": ")) - elif df.fieldtype in ("Currency", "Float", "Percent") and not isinstance(d[fieldname], float): + elif df.fieldtype in float_like_fields and not isinstance(d[fieldname], float): d[fieldname] = flt(d[fieldname]) - elif df.fieldtype in ("Datetime", "Date", "Time") and d[fieldname] == "": + elif (df.fieldtype in datetime_fields and d[fieldname] == "") or ( + getattr(df, "unique", False) and cstr(d[fieldname]).strip() == "" + ): d[fieldname] = None - elif df.get("unique") and cstr(d[fieldname]).strip() == "": - # unique empty field should be set to None - d[fieldname] = None - - if isinstance(d[fieldname], list) and df.fieldtype not in table_fields: - frappe.throw(_("Value for {0} cannot be a list").format(_(df.label))) - if convert_dates_to_str and isinstance( d[fieldname], (datetime.datetime, datetime.date, datetime.time, datetime.timedelta) ): d[fieldname] = str(d[fieldname]) - if d[fieldname] is None and ignore_nulls: + if ignore_nulls and d[fieldname] is None: del d[fieldname] return d @@ -361,7 +363,7 @@ class BaseDocument(object): convert_dates_to_str=False, no_child_table_fields=False, ) -> Dict: - doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str) + doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str, ignore_nulls=no_nulls) doc["doctype"] = self.doctype for df in self.meta.get_table_fields(): @@ -376,20 +378,15 @@ class BaseDocument(object): for d in children ] - if no_nulls: - for k in list(doc): - if doc[k] is None: - del doc[k] - if no_default_fields: - for k in list(doc): - if k in default_fields: - del doc[k] + for key in default_fields: + if key in doc: + del doc[key] if no_child_table_fields: - for k in list(doc): - if k in child_table_fields: - del doc[k] + for key in child_table_fields: + if key in doc: + del doc[key] for key in ( "_user_tags", @@ -399,8 +396,8 @@ class BaseDocument(object): "__run_link_triggers", "__unsaved", ): - if self.get(key): - doc[key] = self.get(key) + if value := getattr(self, key, None): + doc[key] = value return doc From 95816f03408fe85059a7d1b665853cb33229e179 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Sat, 23 Apr 2022 17:59:54 +0530 Subject: [PATCH 56/57] fix: better validation for child insert --- frappe/client.py | 5 ++++- frappe/tests/test_client.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/frappe/client.py b/frappe/client.py index a8223cdeee..1bad2bed2f 100644 --- a/frappe/client.py +++ b/frappe/client.py @@ -189,7 +189,10 @@ def insert(doc=None): if isinstance(doc, str): doc = json.loads(doc) - if doc.get("parenttype"): + doc = frappe._dict(doc) + if frappe.is_table(doc.doctype): + if not (doc.parenttype and doc.parent and doc.parentfield): + frappe.throw(_("parenttype, parent and parentfield are required to insert a child record")) # inserting a child record parent = frappe.get_doc(doc.parenttype, doc.parent) parent.append(doc.parentfield, doc) diff --git a/frappe/tests/test_client.py b/frappe/tests/test_client.py index c86c482651..677f59a366 100644 --- a/frappe/tests/test_client.py +++ b/frappe/tests/test_client.py @@ -141,3 +141,40 @@ class TestClient(unittest.TestCase): self.assertEqual(get("ToDo", filters=filters_json).description, "test") todo.delete() + + def test_client_insert(self): + from frappe.client import insert + + def get_random_title(): + return "test-{0}".format(frappe.generate_hash()) + + # test insert dict + doc = {"doctype": "Note", "title": get_random_title(), "content": "test"} + note1 = insert(doc) + self.assertTrue(note1) + + # test insert json + doc["title"] = get_random_title() + json_doc = frappe.as_json(doc) + note2 = insert(json_doc) + self.assertTrue(note2) + + # test insert child doc without parent fields + child_doc = {"doctype": "Note Seen By", "user": "Administrator"} + with self.assertRaises(frappe.ValidationError): + insert(child_doc) + + # test insert child doc with parent fields + child_doc = { + "doctype": "Note Seen By", + "user": "Administrator", + "parenttype": "Note", + "parent": note1.name, + "parentfield": "seen_by", + } + note3 = insert(child_doc) + self.assertTrue(note3) + + # cleanup + frappe.delete_doc("Note", note1.name) + frappe.delete_doc("Note", note2.name) From 2767ea9d255b9f8c8a2e3840cf7b2df50ce36b56 Mon Sep 17 00:00:00 2001 From: Nikhil Kothari Date: Mon, 25 Apr 2022 15:07:04 +0530 Subject: [PATCH 57/57] feat: more color options for form alerts (#16729) * Added green and orange color options for message --- frappe/public/js/frappe/form/layout.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 578956f0ca..403abf0981 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -98,7 +98,7 @@ frappe.ui.form.Layout = class Layout { // remove previous color this.message.removeClass(this.message_color); } - this.message_color = (color && ['yellow', 'blue', 'red'].includes(color)) ? color : 'blue'; + this.message_color = (color && ['yellow', 'blue', 'red', 'green', 'orange'].includes(color)) ? color : 'blue'; if (html) { if (html.substr(0, 1)!=='<') { // wrap in a block @@ -439,7 +439,7 @@ frappe.ui.form.Layout = class Layout { } handle_tab(doctype, fieldname, shift) { - let grid_row = null, + let grid_row = null, prev = null, fields = this.fields_list, focused = false;