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 @@
+
+
+
+
+
+
+
+
+
+