Merge branch 'develop' into permlevel-apis
This commit is contained in:
commit
2b27652bd4
24 changed files with 310 additions and 97 deletions
7
.github/helper/roulette.py
vendored
7
.github/helper/roulette.py
vendored
|
|
@ -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:
|
||||
|
|
|
|||
2
.github/workflows/patch-mariadb-tests.yml
vendored
2
.github/workflows/patch-mariadb-tests.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/server-tests.yml
vendored
2
.github/workflows/server-tests.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/ui-tests.yml
vendored
2
.github/workflows/ui-tests.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
We appreciate your efforts to responsibly disclose your findings. We'll endeavor to respond quickly, and will keep you updated throughout the process.
|
||||
|
|
|
|||
10
codecov.yml
10
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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 += "<br>" + _("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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -392,4 +392,5 @@ ignore_links_on_delete = [
|
|||
"Email Queue",
|
||||
"Document Share Key",
|
||||
"Integration Request",
|
||||
"Unhandled Email",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
|
|
|||
|
|
@ -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"] = []
|
||||
|
|
|
|||
|
|
@ -48,6 +48,16 @@
|
|||
<path d="M9 3L13 5.99999L9 9" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-unhide">
|
||||
<path stroke="none" fill-rule="evenodd" clip-rule="evenodd" d="M2.10756 9.53547C1.93501 9.82126 1.93501 10.1787 2.10756 10.4645C3.75635 13.1955 6.60531 15 9.84351 15C13.0817 15 15.9307 13.1955 17.5795 10.4645C17.752 10.1787 17.752 9.82127 17.5795 9.53548C15.9307 6.80451 13.0817 5 9.84351 5C6.60531 5 3.75635 6.8045 2.10756 9.53547ZM10 13C11.6569 13 13 11.6569 13 10C13 8.34315 11.6569 7 10 7C8.34315 7 7 8.34315 7 10C7 11.6569 8.34315 13 10 13Z" fill="var(--icon-stroke)"/>
|
||||
<circle cx="10" cy="10" r="1" stroke="none" fill="var(--icon-stroke)"/>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-hide">
|
||||
<rect stroke="none" x="3.02185" y="3.89151" width="1.26078" height="18.4481" rx="0.630391" transform="rotate(-45 3.02185 3.89151)" fill="var(--icon-stroke)"/>
|
||||
<path stroke="none" fill-rule="evenodd" clip-rule="evenodd" d="M5.02016 6.99831C4.84611 6.82426 4.57032 6.80165 4.37821 6.95554C3.49472 7.66323 2.73193 8.53749 2.12941 9.53547C1.95686 9.82126 1.95686 10.1787 2.12941 10.4645C3.7782 13.1955 6.62716 15 9.86536 15C10.5301 15 11.1784 14.924 11.8032 14.7795C12.1655 14.6957 12.2727 14.2508 12.0098 13.9879L11.1052 13.0833C10.9747 12.9529 10.7837 12.9083 10.6027 12.9438C10.4148 12.9807 10.2206 13 10.0219 13C8.365 13 7.02185 11.6569 7.02185 10C7.02185 9.80128 7.04117 9.60707 7.07804 9.41915C7.11355 9.23815 7.06896 9.04711 6.93853 8.91668L5.02016 6.99831ZM12.1967 12.8433C11.9793 12.6259 12.011 12.2666 12.2202 12.0414C12.7176 11.506 13.0219 10.7885 13.0219 10C13.0219 8.34315 11.6787 7 10.0219 7C9.23334 7 8.51587 7.30421 7.98043 7.80167C7.75522 8.0109 7.3959 8.04255 7.17854 7.82518L5.98518 6.63183C5.75274 6.39939 5.80413 6.00935 6.10001 5.86613C7.24996 5.3095 8.52428 5 9.86536 5C13.1036 5 15.9525 6.80451 17.6013 9.53548C17.7739 9.82127 17.7739 10.1787 17.6013 10.4645C16.6787 11.9927 15.3803 13.2307 13.8482 14.0249C13.6613 14.1218 13.4343 14.0809 13.2854 13.932L12.1967 12.8433Z" fill="var(--icon-stroke)"/>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-sidebar-collapse">
|
||||
<path d="M12 6L6 12L12 18" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18 6L12 12L18 18" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 111 KiB |
|
|
@ -78,9 +78,13 @@ frappe.views.Workspace = class Workspace {
|
|||
|
||||
sidebar_item_container(item) {
|
||||
return $(`
|
||||
<div class="sidebar-item-container ${item.is_editable ? "is-draggable" : ""}" item-parent="${
|
||||
item.parent_page
|
||||
}" item-name="${item.title}" item-public="${item.public || 0}">
|
||||
<div
|
||||
class="sidebar-item-container ${item.is_editable ? "is-draggable" : ""}"
|
||||
item-parent="${item.parent_page}"
|
||||
item-name="${item.title}"
|
||||
item-public="${item.public || 0}"
|
||||
item-is-hidden="${item.is_hidden || 0}"
|
||||
>
|
||||
<div class="desk-sidebar-item standard-sidebar-item ${item.selected ? "selected" : ""}">
|
||||
<a
|
||||
href="/app/${
|
||||
|
|
@ -393,6 +397,7 @@ frappe.views.Workspace = class Workspace {
|
|||
this.page.set_secondary_action(__("Edit"), async () => {
|
||||
if (!this.editor || !this.editor.readOnly) return;
|
||||
this.is_read_only = false;
|
||||
this.toggle_hidden_workspaces(true);
|
||||
await this.editor.readOnly.toggle();
|
||||
this.editor.isReady.then(() => {
|
||||
this.initialize_editorjs_undo();
|
||||
|
|
@ -441,6 +446,7 @@ frappe.views.Workspace = class Workspace {
|
|||
this.page.set_secondary_action(__("Discard"), async () => {
|
||||
this.discard = true;
|
||||
this.clear_page_actions();
|
||||
this.toggle_hidden_workspaces(false);
|
||||
await this.editor.readOnly.toggle();
|
||||
this.is_read_only = true;
|
||||
this.sidebar_pages = this.cached_pages;
|
||||
|
|
@ -455,6 +461,10 @@ frappe.views.Workspace = class Workspace {
|
|||
}
|
||||
}
|
||||
|
||||
toggle_hidden_workspaces(show) {
|
||||
$(".desk-sidebar").toggleClass("show-hidden-workspaces", show);
|
||||
}
|
||||
|
||||
show_sidebar_actions() {
|
||||
this.sidebar.find(".standard-sidebar-section").addClass("show-control");
|
||||
this.make_sidebar_sortable();
|
||||
|
|
@ -481,6 +491,15 @@ frappe.views.Workspace = class Workspace {
|
|||
null,
|
||||
sidebar_control
|
||||
);
|
||||
} else if (item.is_hidden) {
|
||||
frappe.utils.add_custom_button(
|
||||
frappe.utils.icon("unhide", "sm"),
|
||||
(e) => this.unhide_workspace(item, e),
|
||||
"unhide-workspace-btn",
|
||||
__("Unhide Workspace"),
|
||||
null,
|
||||
sidebar_control
|
||||
);
|
||||
} else {
|
||||
frappe.utils.add_custom_button(
|
||||
frappe.utils.icon("drag", "xs"),
|
||||
|
|
@ -720,6 +739,12 @@ frappe.views.Workspace = class Workspace {
|
|||
icon: frappe.utils.icon("duplicate", "sm"),
|
||||
action: () => this.duplicate_page(item),
|
||||
},
|
||||
{
|
||||
label: __("Hide"),
|
||||
title: __("Hide Workspace"),
|
||||
icon: frappe.utils.icon("hide", "sm"),
|
||||
action: (e) => this.hide_workspace(item, e),
|
||||
},
|
||||
];
|
||||
|
||||
if (this.is_item_deletable(item)) {
|
||||
|
|
@ -748,7 +773,7 @@ frappe.views.Workspace = class Workspace {
|
|||
|
||||
html.click((event) => {
|
||||
event.stopPropagation();
|
||||
action && action();
|
||||
action && action(event);
|
||||
});
|
||||
|
||||
return html;
|
||||
|
|
@ -910,6 +935,47 @@ frappe.views.Workspace = class Workspace {
|
|||
d.show();
|
||||
}
|
||||
|
||||
hide_unhide_workspace(page, event, hide) {
|
||||
page.is_hidden = hide;
|
||||
|
||||
let sidebar_control = event.target.closest(".sidebar-item-control");
|
||||
let sidebar_item_container = sidebar_control.closest(".sidebar-item-container");
|
||||
$(sidebar_item_container).attr("item-is-hidden", hide);
|
||||
|
||||
$(sidebar_control).empty();
|
||||
this.add_sidebar_actions(page, $(sidebar_control));
|
||||
|
||||
this.add_drop_icon(page, $(sidebar_control), $(sidebar_item_container));
|
||||
|
||||
let cached_page = this.cached_pages.pages.findIndex((p) => p.name === page.name);
|
||||
if (cached_page !== -1) {
|
||||
this.cached_pages.pages[cached_page].is_hidden = hide;
|
||||
}
|
||||
|
||||
let method = hide ? "hide_page" : "unhide_page";
|
||||
frappe.call({
|
||||
method: "frappe.desk.doctype.workspace.workspace." + method,
|
||||
args: {
|
||||
page_name: page.name,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (!r.message) return;
|
||||
|
||||
let message = hide ? "{0} is hidden successfully" : "{0} is unhidden successfully";
|
||||
message = __(message, [page.title.bold()]);
|
||||
frappe.show_alert({ message: message, indicator: "green" });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
hide_workspace(page, event) {
|
||||
this.hide_unhide_workspace(page, event, 1);
|
||||
}
|
||||
|
||||
unhide_workspace(page, event) {
|
||||
this.hide_unhide_workspace(page, event, 0);
|
||||
}
|
||||
|
||||
make_sidebar_sortable() {
|
||||
let me = this;
|
||||
$(".nested-container").each(function () {
|
||||
|
|
|
|||
|
|
@ -1017,7 +1017,29 @@ body {
|
|||
|
||||
.sidebar-item-container {
|
||||
position: relative;
|
||||
.sidebar-item-container{
|
||||
|
||||
&[item-is-hidden="1"] {
|
||||
display: none;
|
||||
opacity: 0.4;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&[item-is-hidden="0"] {
|
||||
.drop-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:has(.sidebar-item-container[item-is-hidden="0"]) {
|
||||
.drop-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-item-container {
|
||||
margin-left: 10px;
|
||||
|
||||
.standard-sidebar-item {
|
||||
|
|
@ -1026,31 +1048,64 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
.standard-sidebar-section.show-control {
|
||||
.desk-sidebar-item.standard-sidebar-item {
|
||||
.desk-sidebar {
|
||||
&.show-hidden-workspaces {
|
||||
|
||||
&:hover, &.selected {
|
||||
.drag-handle {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.setting-btn, .duplicate-page {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.drop-icon {
|
||||
padding: 10px 8px 10px 2px;
|
||||
margin-left: -8px;
|
||||
}
|
||||
.unhide-workspace-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.block-click {
|
||||
pointer-events:none;
|
||||
.standard-sidebar-section {
|
||||
display: block;
|
||||
|
||||
.sidebar-item-container {
|
||||
&[item-is-hidden="1"] {
|
||||
display: block;
|
||||
}
|
||||
&[item-is-hidden="0"] {
|
||||
.drop-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.standard-sidebar-section {
|
||||
display: none;
|
||||
|
||||
&:has(> [item-is-hidden="0"]) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.standard-sidebar-section.show-control {
|
||||
.desk-sidebar-item.standard-sidebar-item {
|
||||
|
||||
&:hover, &.selected {
|
||||
.drag-handle {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.setting-btn, .duplicate-page, .unhide-workspace-btn {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.drop-icon {
|
||||
padding: 10px 8px 10px 2px;
|
||||
margin-left: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
.block-click {
|
||||
pointer-events:none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.codex-editor__loader {
|
||||
display: none !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -495,7 +495,7 @@ def setup_image_doctype():
|
|||
|
||||
@whitelist_for_tests
|
||||
def setup_inbox():
|
||||
frappe.db.sql("DELETE FROM `tabUser Email`")
|
||||
frappe.db.delete("User Email")
|
||||
|
||||
user = frappe.get_doc("User", frappe.session.user)
|
||||
user.append("user_emails", {"email_account": "Email Linking"})
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import logging
|
||||
import os
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from typing import Literal
|
||||
|
||||
# imports - module imports
|
||||
import frappe
|
||||
|
|
@ -95,7 +96,7 @@ class SiteContextFilter(logging.Filter):
|
|||
return True
|
||||
|
||||
|
||||
def set_log_level(level: int) -> None:
|
||||
def set_log_level(level: Literal["ERROR", "WARNING", "WARN", "INFO", "DEBUG"]) -> None:
|
||||
"""Use this method to set log level to something other than the default DEBUG"""
|
||||
frappe.log_level = getattr(logging, (level or "").upper(), None) or default_log_level
|
||||
frappe.loggers = {}
|
||||
|
|
|
|||
|
|
@ -118,10 +118,10 @@ def get_rendered_template(
|
|||
validate_print_permission(doc)
|
||||
|
||||
if doc.meta.is_submittable:
|
||||
if doc.docstatus == 0 and not cint(print_settings.allow_print_for_draft):
|
||||
if doc.docstatus.is_draft() and not cint(print_settings.allow_print_for_draft):
|
||||
frappe.throw(_("Not allowed to print draft documents"), frappe.PermissionError)
|
||||
|
||||
if doc.docstatus == 2 and not cint(print_settings.allow_print_for_cancelled):
|
||||
if doc.docstatus.is_cancelled() and not cint(print_settings.allow_print_for_cancelled):
|
||||
frappe.throw(_("Not allowed to print cancelled documents"), frappe.PermissionError)
|
||||
|
||||
doc.run_method("before_print", print_settings)
|
||||
|
|
|
|||
|
|
@ -15,18 +15,19 @@ def get_context(context):
|
|||
|
||||
host = get_request_site_address()
|
||||
|
||||
blog_list = frappe.db.sql(
|
||||
"""\
|
||||
select route as name, published_on, modified, title, content from `tabBlog Post`
|
||||
where ifnull(published,0)=1
|
||||
order by published_on desc limit 20""",
|
||||
as_dict=1,
|
||||
blog_list = frappe.get_all(
|
||||
"Blog Post",
|
||||
fields=["name", "published_on", "modified", "title", "content"],
|
||||
filters={"published": 1},
|
||||
order_by="published_on desc",
|
||||
limit=20,
|
||||
)
|
||||
|
||||
for blog in blog_list:
|
||||
blog_page = cstr(quote(blog.name.encode("utf-8")))
|
||||
blog.link = urljoin(host, blog_page)
|
||||
blog.content = escape_html(blog.content or "")
|
||||
blog.title = escape_html(blog.title or "")
|
||||
|
||||
if blog_list:
|
||||
modified = max(blog["modified"] for blog in blog_list)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue