diff --git a/.github/helper/roulette.py b/.github/helper/roulette.py index f2a8bcb700..e3b212fa89 100644 --- a/.github/helper/roulette.py +++ b/.github/helper/roulette.py @@ -23,10 +23,15 @@ def fetch_pr_data(pr_number, repo, endpoint=""): def req(url): "Simple resilient request call to handle rate limits." + headers = None + token = os.environ.get("GITHUB_TOKEN") + if token: + headers = {"authorization": f"Bearer {token}"} + retries = 0 while True: try: - req = urllib.request.Request(url) + req = urllib.request.Request(url, headers=headers) return urllib.request.urlopen(req) except HTTPError as exc: if exc.code == 403 and retries < 5: diff --git a/.github/workflows/patch-mariadb-tests.yml b/.github/workflows/patch-mariadb-tests.yml index 57f421cc1b..4b487d2aea 100644 --- a/.github/workflows/patch-mariadb-tests.yml +++ b/.github/workflows/patch-mariadb-tests.yml @@ -9,6 +9,7 @@ concurrency: cancel-in-progress: true permissions: + # Do not change this as GITHUB_TOKEN is being used by roulette contents: read jobs: @@ -31,6 +32,7 @@ jobs: TYPE: "server" PR_NUMBER: ${{ github.event.number }} REPO_NAME: ${{ github.repository }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} test: name: Patch diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index cc310fd8d7..3b76da1973 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -12,6 +12,7 @@ concurrency: permissions: + # Do not change this as GITHUB_TOKEN is being used by roulette contents: read jobs: @@ -34,6 +35,7 @@ jobs: TYPE: "server" PR_NUMBER: ${{ github.event.number }} REPO_NAME: ${{ github.repository }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} test: name: Unit Tests diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index f022f10ea4..1b88bc73ce 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -11,6 +11,7 @@ concurrency: cancel-in-progress: true permissions: + # Do not change this as GITHUB_TOKEN is being used by roulette contents: read jobs: @@ -33,6 +34,7 @@ jobs: TYPE: "ui" PR_NUMBER: ${{ github.event.number }} REPO_NAME: ${{ github.repository }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} test: runs-on: ubuntu-latest diff --git a/SECURITY.md b/SECURITY.md index 1126c8b4da..cbe6016713 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,7 +1,7 @@ # Security Policy -The Frappe team and community take security issues in the Frappe Framework seriously. To report a security issue, fill out the form at [https://erpnext.com/security/report](https://erpnext.com/security/report). +The Frappe team and community take security issues in the Frappe Framework seriously. To report a security issue, please go through the information mentioned [here](https://frappe.io/security). You can help us make Frappe and consequently all Frappe dependent apps like [ERPNext](https://erpnext.com) more secure by following the [Reporting guidelines](https://erpnext.com/security). -We appreciate your efforts to responsibly disclose your findings. We'll endeavor to respond quickly, and will keep you updated throughout the process. \ No newline at end of file +We appreciate your efforts to responsibly disclose your findings. We'll endeavor to respond quickly, and will keep you updated throughout the process. diff --git a/codecov.yml b/codecov.yml index b16c49c8d6..3fca9d9384 100644 --- a/codecov.yml +++ b/codecov.yml @@ -9,7 +9,7 @@ coverage: target: auto threshold: 0.5% flags: - - server-mariadb + - server patch: default: target: 85% @@ -17,7 +17,7 @@ coverage: only_pulls: true if_ci_failed: ignore flags: - - server-mariadb + - server comment: layout: "diff, flags" @@ -25,11 +25,7 @@ comment: show_critical_paths: true flags: - server-mariadb: - paths: - - "**/*.py" - carryforward: true - server-postgres: + server: paths: - "**/*.py" carryforward: true diff --git a/cypress/integration/workspace.js b/cypress/integration/workspace.js index e7d97c705b..d52417b234 100644 --- a/cypress/integration/workspace.js +++ b/cypress/integration/workspace.js @@ -190,6 +190,48 @@ context("Workspace 2.0", () => { cy.get('.standard-actions .btn-primary[data-label="Save"]').click(); }); + it("Hide/Unhide Workspaces", () => { + // hide + cy.intercept({ + method: "POST", + url: "api/method/frappe.desk.doctype.workspace.workspace.hide_page", + }).as("hide_page"); + + cy.get(".codex-editor__redactor .ce-block"); + cy.get(".standard-actions .btn-secondary[data-label=Edit]").click(); + + cy.get('.sidebar-item-container[item-name="Duplicate Page"]') + .find(".sidebar-item-control .setting-btn") + .click(); + cy.get('.sidebar-item-container[item-name="Duplicate Page"]') + .find('.dropdown-item[title="Hide Workspace"]') + .click({ force: true }); + cy.wait(300); + cy.get('.standard-actions .btn-secondary[data-label="Discard"]').click(); + cy.get('.sidebar-item-container[item-name="Duplicate Page"]').should("not.be.visible"); + + cy.wait("@hide_page"); + + // unhide + cy.intercept({ + method: "POST", + url: "api/method/frappe.desk.doctype.workspace.workspace.unhide_page", + }).as("unhide_page"); + + cy.get(".codex-editor__redactor .ce-block"); + cy.get(".standard-actions .btn-secondary[data-label=Edit]").click(); + + cy.get('.sidebar-item-container[item-name="Duplicate Page"]') + .find('[title="Unhide Workspace"]') + .click({ force: true }); + cy.wait(300); + + cy.get('.standard-actions .btn-secondary[data-label="Discard"]').click(); + cy.get('.sidebar-item-container[item-name="Duplicate Page"]').should("be.visible"); + + cy.wait("@unhide_page"); + }); + it("Delete Duplicate Page", () => { cy.intercept({ method: "POST", diff --git a/frappe/core/doctype/log_settings/log_settings.py b/frappe/core/doctype/log_settings/log_settings.py index e1e1d5d176..eaa55f4bca 100644 --- a/frappe/core/doctype/log_settings/log_settings.py +++ b/frappe/core/doctype/log_settings/log_settings.py @@ -42,27 +42,26 @@ def _supports_log_clearing(doctype: str) -> bool: class LogSettings(Document): def validate(self): - self.validate_supported_doctypes() - self.validate_duplicates() + self._remove_unsupported_doctypes() + self._deduplicate_entries() self.add_default_logtypes() - def validate_supported_doctypes(self): - for entry in self.logs_to_clear: + def _remove_unsupported_doctypes(self): + for entry in list(self.logs_to_clear): if _supports_log_clearing(entry.ref_doctype): continue msg = _("{} does not support automated log clearing.").format(frappe.bold(entry.ref_doctype)) if frappe.conf.developer_mode: msg += "
" + _("Implement `clear_old_logs` method to enable auto error clearing.") - frappe.throw(msg, title=_("DocType not supported by Log Settings.")) + frappe.msgprint(msg, title=_("DocType not supported by Log Settings.")) + self.remove(entry) - def validate_duplicates(self): + def _deduplicate_entries(self): seen = set() - for entry in self.logs_to_clear: + for entry in list(self.logs_to_clear): if entry.ref_doctype in seen: - frappe.throw( - _("{} appears more than once in configured log doctypes.").format(entry.ref_doctype) - ) + self.remove(entry) seen.add(entry.ref_doctype) def add_default_logtypes(self): diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py index 1173fa3591..13525d2328 100644 --- a/frappe/database/mariadb/schema.py +++ b/frappe/database/mariadb/schema.py @@ -1,3 +1,5 @@ +from pymysql.constants.ER import DUP_ENTRY + import frappe from frappe import _ from frappe.database.schema import DBTable @@ -115,17 +117,15 @@ class MariaDBTable(DBTable): frappe.db.sql(query) except Exception as e: - # sanitize - if e.args[0] == 1060: - frappe.throw(str(e)) - elif e.args[0] == 1062: + if query := locals().get("query"): # this weirdness is to avoid potentially unbounded vars + print(f"Failed to alter schema using query: {query}") + + if e.args[0] == DUP_ENTRY: fieldname = str(e).split("'")[-2] frappe.throw( _("{0} field cannot be set as unique in {1}, as there are non-unique existing values").format( fieldname, self.table_name ) ) - elif e.args[0] == 1067: - frappe.throw(str(e.args[1])) - else: - raise e + + raise diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index dfe7850349..f2243c2e56 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -379,7 +379,17 @@ def get_workspace_sidebar_items(): # pages sorted based on sequence id order_by = "sequence_id asc" - fields = ["name", "title", "for_user", "parent_page", "content", "public", "module", "icon"] + fields = [ + "name", + "title", + "for_user", + "parent_page", + "content", + "public", + "module", + "icon", + "is_hidden", + ] all_pages = frappe.get_all( "Workspace", fields=fields, filters=filters, order_by=order_by, ignore_permissions=True ) @@ -391,7 +401,7 @@ def get_workspace_sidebar_items(): try: workspace = Workspace(page, True) if has_access or workspace.is_permitted(): - if page.public: + if page.public and (has_access or not page.is_hidden): pages.append(page) elif page.for_user == frappe.session.user: private_pages.append(page) diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json index 58a1f718fd..edd5c32e99 100644 --- a/frappe/desk/doctype/workspace/workspace.json +++ b/frappe/desk/doctype/workspace/workspace.json @@ -19,6 +19,7 @@ "restrict_to_domain", "hide_custom", "public", + "is_hidden", "content", "tab_break_2", "charts", @@ -174,11 +175,17 @@ "fieldtype": "Table", "label": "Quick Lists", "options": "Workspace Quick List" + }, + { + "default": "0", + "fieldname": "is_hidden", + "fieldtype": "Check", + "label": "Is Hidden" } ], "in_create": 1, "links": [], - "modified": "2023-01-07 17:02:48.278025", + "modified": "2023-01-07 19:37:39.512482", "modified_by": "Administrator", "module": "Desk", "name": "Workspace", diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index c4110f52a7..f7d9e8ac3e 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -247,6 +247,32 @@ def update_page(name, title, icon, parent, public): return {"name": title, "public": public, "label": new_name} +def hide_unhide_page(page_name: str, is_hidden: bool): + page = frappe.get_doc("Workspace", page_name) + + if page.get("public") and not is_workspace_manager(): + frappe.throw( + _("Need Workspace Manager role to hide/unhide public workspaces"), frappe.PermissionError + ) + + if not page.get("public") and page.get("for_user") != frappe.session.user: + frappe.throw(_("Cannot update private workspace of other users"), frappe.PermissionError) + + page.is_hidden = int(is_hidden) + page.save(ignore_permissions=True) + return True + + +@frappe.whitelist() +def hide_page(page_name: str): + return hide_unhide_page(page_name, 1) + + +@frappe.whitelist() +def unhide_page(page_name: str): + return hide_unhide_page(page_name, 0) + + @frappe.whitelist() def duplicate_page(page_name, new_page): if not loads(new_page): diff --git a/frappe/desk/form/save.py b/frappe/desk/form/save.py index f43031c899..9ee2541a90 100644 --- a/frappe/desk/form/save.py +++ b/frappe/desk/form/save.py @@ -6,6 +6,7 @@ import json import frappe from frappe.core.doctype.submission_queue.submission_queue import queue_submission from frappe.desk.form.load import run_onload +from frappe.model.docstatus import DocStatus from frappe.monitor import add_data_to_monitor from frappe.utils.scheduler import is_scheduler_inactive @@ -17,8 +18,14 @@ def savedocs(doc, action): set_local_name(doc) # action - doc.docstatus = {"Save": 0, "Submit": 1, "Update": 1, "Cancel": 2}[action] - if doc.docstatus == 1: + doc.docstatus = { + "Save": DocStatus.draft(), + "Submit": DocStatus.submitted(), + "Update": DocStatus.submitted(), + "Cancel": DocStatus.cancelled(), + }[action] + + if doc.docstatus.is_submitted(): if action == "Submit" and doc.meta.queue_in_background and not is_scheduler_inactive(): queue_submission(doc, action) return diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 593e6bf0c2..8f929311e0 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -294,7 +294,7 @@ def save_report(name, doctype, report_settings): if report.report_type != "Report Builder": frappe.throw(_("Only reports of type Report Builder can be edited")) - if report.owner != frappe.session.user and not frappe.has_permission("Report", "write"): + if report.owner != frappe.session.user and not report.has_permission("write"): frappe.throw(_("Insufficient Permissions for editing Report"), frappe.PermissionError) else: report = frappe.new_doc("Report") @@ -323,7 +323,7 @@ def delete_report(name): if report.report_type != "Report Builder": frappe.throw(_("Only reports of type Report Builder can be deleted")) - if report.owner != frappe.session.user and not frappe.has_permission("Report", "delete"): + if report.owner != frappe.session.user and not report.has_permission("delete"): frappe.throw(_("Insufficient Permissions for deleting Report"), frappe.PermissionError) report.delete(ignore_permissions=True) diff --git a/frappe/hooks.py b/frappe/hooks.py index 9ad95e796b..28b2c0dea1 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -392,4 +392,5 @@ ignore_links_on_delete = [ "Email Queue", "Document Share Key", "Integration Request", + "Unhandled Email", ] diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 1836896a9e..48eaa63460 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -9,6 +9,7 @@ import frappe.defaults import frappe.model.meta from frappe import _, get_module_path from frappe.desk.doctype.tag.tag import delete_tags_for_document +from frappe.model.docstatus import DocStatus from frappe.model.dynamic_links import get_dynamic_link_map from frappe.model.naming import revert_series_if_last from frappe.model.utils import is_virtual_doctype @@ -265,7 +266,7 @@ def check_if_doc_is_linked(doc, method="Delete"): # don't check for communication and todo! continue - if method != "Delete" and (method != "Cancel" or item.docstatus != 1): + if method != "Delete" and (method != "Cancel" or not DocStatus(item.docstatus).is_submitted()): # don't raise exception if not # linked to a non-cancelled doc when deleting or to a submitted doc when cancelling continue @@ -302,13 +303,12 @@ def check_if_doc_is_dynamically_linked(doc, method="Delete"): refdoc.get(df.options) == doc.doctype and refdoc.get(df.fieldname) == doc.name and ( - (method == "Delete" and refdoc.docstatus < 2) - or (method == "Cancel" and refdoc.docstatus == 1) + # linked to an non-cancelled doc when deleting + (method == "Delete" and not DocStatus(refdoc.docstatus).is_cancelled()) + # linked to a submitted doc when cancelling + or (method == "Cancel" and DocStatus(refdoc.docstatus).is_submitted()) ) ): - # raise exception only if - # linked to an non-cancelled doc when deleting - # or linked to a submitted doc when cancelling raise_link_exists_exception(doc, df.parent, df.parent) else: # dynamic link in table @@ -321,14 +321,11 @@ def check_if_doc_is_dynamically_linked(doc, method="Delete"): (doc.doctype, doc.name), as_dict=True, ): - - if (method == "Delete" and refdoc.docstatus < 2) or ( - method == "Cancel" and refdoc.docstatus == 1 + # linked to an non-cancelled doc when deleting + # or linked to a submitted doc when cancelling + if (method == "Delete" and not DocStatus(refdoc.docstatus).is_cancelled()) or ( + method == "Cancel" and DocStatus(refdoc.docstatus).is_submitted() ): - # raise exception only if - # linked to an non-cancelled doc when deleting - # or linked to a submitted doc when cancelling - reference_doctype = refdoc.parenttype if meta.istable else df.parent reference_docname = refdoc.parent if meta.istable else refdoc.name at_position = f"at Row: {refdoc.idx}" if meta.istable else "" diff --git a/frappe/permissions.py b/frappe/permissions.py index af4d4178a3..ef33c03875 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -27,22 +27,6 @@ rights = ( ) -def check_admin_or_system_manager(user=None): - from frappe.utils.commands import warn - - warn( - "The function check_admin_or_system_manager will be deprecated in version 15." - 'Please use frappe.only_for("System Manager") instead.', - category=PendingDeprecationWarning, - ) - - if not user: - user = frappe.session.user - - if ("System Manager" not in frappe.get_roles(user)) and (user != "Administrator"): - frappe.throw(_("Not permitted"), frappe.PermissionError) - - def print_has_permission_check_logs(func): def inner(*args, **kwargs): frappe.flags["has_permission_check_logs"] = [] diff --git a/frappe/public/icons/timeless/icons.svg b/frappe/public/icons/timeless/icons.svg index 69e9e920e6..fed77e7df1 100644 --- a/frappe/public/icons/timeless/icons.svg +++ b/frappe/public/icons/timeless/icons.svg @@ -48,6 +48,16 @@ + + + + + + + + + + diff --git a/frappe/public/js/frappe/views/workspace/workspace.js b/frappe/public/js/frappe/views/workspace/workspace.js index cbcea38f38..ee5d2c92a0 100644 --- a/frappe/public/js/frappe/views/workspace/workspace.js +++ b/frappe/public/js/frappe/views/workspace/workspace.js @@ -78,9 +78,13 @@ frappe.views.Workspace = class Workspace { sidebar_item_container(item) { return $(` -