Merge branch 'develop' into feat-desk-refresh

This commit is contained in:
Ankush Menat 2023-09-18 21:05:37 +05:30
commit 1ebbbc772e
44 changed files with 600 additions and 237 deletions

View file

@ -64,9 +64,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: |
3.7
3.10
python-version: "3.10"
- name: Setup Node
uses: actions/setup-node@v3
@ -112,8 +110,8 @@ jobs:
- name: Run Patch Tests
run: |
cd ~/frappe-bench/
wget https://frappeframework.com/files/v10-frappe.sql.gz
bench --site test_site --force restore ~/frappe-bench/v10-frappe.sql.gz
wget https://frappeframework.com/files/v13-frappe.sql.gz
bench --site test_site --force restore ~/frappe-bench/v13-frappe.sql.gz
source env/bin/activate
cd apps/frappe/
@ -121,7 +119,6 @@ jobs:
function update_to_version() {
version=$1
py=$2
branch_name="version-$version-hotfix"
echo "Updating to v$version"
@ -130,22 +127,22 @@ jobs:
pgrep honcho | xargs kill
rm -rf ~/frappe-bench/env
bench -v setup env --python $py
bench start &> ~/frappe-bench/bench_start.log &
bench -v setup env
bench start &>> ~/frappe-bench/bench_start.log &
bench --site test_site migrate
}
update_to_version 12 python3.7
update_to_version 13 python3.7
update_to_version 14 python3.10
update_to_version 14
echo "Updating to last commit"
pgrep honcho | xargs kill
rm -rf ~/frappe-bench/env
git checkout -q -f "$GITHUB_SHA"
bench -v setup env
bench start &>> ~/frappe-bench/bench_start.log &
bench --site test_site migrate
bench --site test_site execute frappe.tests.utils.check_orpahned_doctypes
- name: Show bench output
if: ${{ always() }}

View file

@ -47,6 +47,17 @@ context("Control Currency", () => {
df_options: { precision: 0 },
blur_expected: "10",
},
{
input: "10.000",
number_format: "#.###,##",
df_options: { precision: 0 },
blur_expected: "10.000",
},
{
input: "10.000",
number_format: "#.###,##",
blur_expected: "10.000,00",
},
{
input: "10.101",
df_options: { precision: "" },
@ -61,6 +72,7 @@ context("Control Currency", () => {
.then((frappe) => {
frappe.boot.sysdefaults.currency = test_case.currency;
frappe.boot.sysdefaults.currency_precision = test_case.default_precision ?? 2;
frappe.boot.sysdefaults.number_format = test_case.number_format ?? "#,###.##";
});
get_dialog_with_currency(test_case.df_options).as("dialog");

View file

@ -83,6 +83,23 @@ context("Control Float", () => {
},
],
},
{
// '.' is the parseFloat's decimal separator
number_format: "#.###,##",
values: [
{
input: "12.345",
blur_expected: "12.345,000",
focus_expected: "12345",
},
{
// parseFloat would reduce 12,340 to 12,34 if this string was ever to be parsed
input: "12.340",
blur_expected: "12.340,000",
focus_expected: "12340",
},
],
},
];
}
});

View file

@ -49,7 +49,7 @@ context("Rounding behaviour", () => {
let rounding_method = "Banker's Rounding";
expect(flt("0.5", 0, null, rounding_method)).eq(0);
expect(flt("0.3", null, rounding_method)).eq(0.3);
expect(flt("0.3", null, null, rounding_method)).eq(0.3);
expect(flt("1.5", 0, null, rounding_method)).eq(2);

View file

@ -170,6 +170,8 @@ lang = local("lang")
# This if block is never executed when running the code. It is only used for
# telling static code analyzer where to find dynamically defined attributes.
if TYPE_CHECKING:
from werkzeug.wrappers import Request
from frappe.database.mariadb.database import MariaDBDatabase
from frappe.database.postgres.database import PostgresDatabase
from frappe.model.document import Document
@ -179,6 +181,15 @@ if TYPE_CHECKING:
db: MariaDBDatabase | PostgresDatabase
qb: MariaDB | Postgres
cache: RedisWrapper
response: _dict
conf: _dict
form_dict: _dict
flags: _dict
request: Request
session: _dict
user: str
flags: _dict
lang: str
# end: static analysis hack

View file

@ -18,8 +18,11 @@ from frappe.core.doctype.doctype.doctype import (
WrongOptionsDoctypeLinkError,
validate_links_table_fieldnames,
)
from frappe.core.doctype.rq_job.test_rq_job import wait_for_completion
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.desk.form.load import getdoc
from frappe.model.delete_doc import delete_controllers
from frappe.model.sync import remove_orphan_doctypes
from frappe.tests.utils import FrappeTestCase
@ -739,6 +742,21 @@ class TestDocType(FrappeTestCase):
self.assertEqual(frappe.get_meta(doctype).get_field(field).default, "DELETETHIS")
frappe.delete_doc("DocType", doctype)
@unittest.skipUnless(
os.access(frappe.get_app_path("frappe"), os.W_OK), "Only run if frappe app paths is writable"
)
@patch.dict(frappe.conf, {"developer_mode": 1})
def test_delete_orphaned_doctypes(self):
doctype = new_doctype(custom=0).insert()
frappe.db.commit()
delete_controllers(doctype.name, doctype.module)
job = frappe.enqueue(remove_orphan_doctypes)
wait_for_completion(job)
frappe.db.rollback()
self.assertFalse(frappe.db.exists("DocType", doctype.name))
def test_not_in_list_view_for_not_allowed_mandatory_field(self):
doctype = new_doctype(
fields=[

View file

@ -86,7 +86,7 @@ class Report(Document):
if (
self.is_standard == "Yes"
and not cint(getattr(frappe.local.conf, "developer_mode", 0))
and not frappe.flags.in_patch
and not (frappe.flags.in_migrate or frappe.flags.in_patch)
):
frappe.throw(_("You are not allowed to delete Standard Report"))
delete_custom_role("report", self.name)

View file

@ -15,16 +15,20 @@ from frappe.utils import cstr, execute_in_shell
from frappe.utils.background_jobs import get_job_status, is_job_enqueued
@timeout(seconds=20)
def wait_for_completion(job: Job):
while True:
if not (job.is_queued or job.is_started):
break
time.sleep(0.2)
class TestRQJob(FrappeTestCase):
BG_JOB = "frappe.core.doctype.rq_job.test_rq_job.test_func"
@timeout(seconds=20)
def check_status(self, job: Job, status, wait=True):
while wait:
if not (job.is_queued or job.is_started):
break
time.sleep(0.2)
if wait:
wait_for_completion(job)
self.assertEqual(frappe.get_doc("RQ Job", job.id).status, status)
def test_serialization(self):

View file

@ -44,22 +44,23 @@ frappe.PermissionEngine = class PermissionEngine {
}
setup_page() {
this.doctype_select = this.wrapper.page
.add_select(
__("Document Type"),
[{ value: "", label: __("Select Document Type") + "..." }].concat(
this.options.doctypes
)
)
.change(function () {
frappe.set_route("permission-manager", $(this).val());
});
this.doctype_select = this.wrapper.page.add_field({
fieldname: "doctype_select",
label: __("Document Type"),
fieldtype: "Link",
options: "DocType",
change: function () {
frappe.set_route("permission-manager", this.get_value());
},
});
this.role_select = this.wrapper.page
.add_select(__("Roles"), [__("Select Role") + "..."].concat(this.options.roles))
.change(() => {
this.refresh();
});
this.role_select = this.wrapper.page.add_field({
fieldname: "role_select",
label: __("Roles"),
fieldtype: "Link",
options: "Role",
change: () => this.refresh(),
});
this.page.add_inner_button(__("Set User Permissions"), () => {
return frappe.set_route("List", "User Permission");
@ -76,13 +77,13 @@ frappe.PermissionEngine = class PermissionEngine {
return;
}
if (frappe.get_route()[1]) {
this.doctype_select.val(frappe.get_route()[1]);
this.doctype_select.set_value(frappe.get_route()[1]);
} else if (frappe.route_options) {
if (frappe.route_options.doctype) {
this.doctype_select.val(frappe.route_options.doctype);
this.doctype_select.set_value(frappe.route_options.doctype);
}
if (frappe.route_options.role) {
this.role_select.val(frappe.route_options.role);
this.role_select.set_value(frappe.route_options.role);
}
frappe.route_options = null;
}
@ -140,13 +141,11 @@ frappe.PermissionEngine = class PermissionEngine {
}
get_doctype() {
let doctype = this.doctype_select.val();
return this.doctype_select.get(0).selectedIndex == 0 ? null : doctype;
return this.doctype_select.get_value();
}
get_role() {
let role = this.role_select.val();
return this.role_select.get(0).selectedIndex == 0 ? null : role;
return this.role_select.get_value();
}
set_empty_message(message) {
@ -292,6 +291,7 @@ frappe.PermissionEngine = class PermissionEngine {
.attr("data-ptype", fieldname)
.attr("data-role", d.role)
.attr("data-permlevel", d.permlevel)
.attr("data-if_owner", d.if_owner)
.attr("data-doctype", d.parent);
checkbox.find("label").css("text-transform", "capitalize");
@ -371,6 +371,7 @@ frappe.PermissionEngine = class PermissionEngine {
doctype: d.parent,
role: d.role,
permlevel: d.permlevel,
if_owner: d.if_owner,
},
callback: (r) => {
if (r.exc) {
@ -399,6 +400,7 @@ frappe.PermissionEngine = class PermissionEngine {
doctype: chk.attr("data-doctype"),
ptype: chk.attr("data-ptype"),
value: chk.prop("checked") ? 1 : 0,
if_owner: chk.attr("data-if_owner"),
};
return frappe.call({
module: "frappe.core",

View file

@ -109,7 +109,7 @@ def add(parent, role, permlevel):
@frappe.whitelist()
def update(doctype, role, permlevel, ptype, value=None):
def update(doctype, role, permlevel, ptype, value=None, if_owner=0):
"""Update role permission params
Args:
@ -127,7 +127,7 @@ def update(doctype, role, permlevel, ptype, value=None):
frappe.clear_cache(doctype=doctype)
frappe.only_for("System Manager")
out = update_permission_property(doctype, role, permlevel, ptype, value)
out = update_permission_property(doctype, role, permlevel, ptype, value, if_owner=if_owner)
frappe.db.after_commit.add(clear_cache)
@ -135,11 +135,14 @@ def update(doctype, role, permlevel, ptype, value=None):
@frappe.whitelist()
def remove(doctype, role, permlevel):
def remove(doctype, role, permlevel, if_owner=0):
frappe.only_for("System Manager")
setup_custom_perms(doctype)
frappe.db.delete("Custom DocPerm", {"parent": doctype, "role": role, "permlevel": permlevel})
frappe.db.delete(
"Custom DocPerm",
{"parent": doctype, "role": role, "permlevel": permlevel, "if_owner": if_owner},
)
if not frappe.get_all("Custom DocPerm", {"parent": doctype}):
frappe.throw(_("There must be atleast one permission rule."), title=_("Cannot Remove"))

View file

@ -52,7 +52,7 @@ class ModuleOnboarding(Document):
is_complete = [bool(step.is_complete or step.is_skipped) for step in steps]
if all(is_complete):
self.is_complete = True
self.save()
self.save(ignore_permissions=True)
return True
return False

View file

@ -2,6 +2,7 @@
# License: MIT. See LICENSE
import json
import typing
from urllib.parse import quote
import frappe
@ -14,6 +15,9 @@ from frappe.model.utils.user_settings import get_user_settings
from frappe.permissions import get_doc_permissions
from frappe.utils.data import cstr
if typing.TYPE_CHECKING:
from frappe.model.document import Document
@frappe.whitelist()
def getdoc(doctype, name, user=None):
@ -117,7 +121,7 @@ def get_docinfo(doc=None, doctype=None, name=None):
"assignments": get_assignments(doc.doctype, doc.name),
"permissions": get_doc_permissions(doc),
"shared": get_docshares(doc),
"views": get_view_logs(doc.doctype, doc.name),
"views": get_view_logs(doc),
"energy_point_logs": get_point_logs(doc.doctype, doc.name),
"additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name),
"milestones": get_milestones(doc.doctype, doc.name),
@ -149,29 +153,22 @@ def add_comments(doc, docinfo):
)
for c in comments:
if c.comment_type == "Comment":
c.content = frappe.utils.markdown(c.content)
docinfo.comments.append(c)
elif c.comment_type in ("Shared", "Unshared"):
docinfo.shared.append(c)
elif c.comment_type in ("Assignment Completed", "Assigned"):
docinfo.assignment_logs.append(c)
elif c.comment_type in ("Attachment", "Attachment Removed"):
docinfo.attachment_logs.append(c)
elif c.comment_type in ("Info", "Edit", "Label"):
docinfo.info_logs.append(c)
elif c.comment_type == "Like":
docinfo.like_logs.append(c)
elif c.comment_type == "Workflow":
docinfo.workflow_logs.append(c)
frappe.utils.add_user_info(c.owner, docinfo.user_info)
match c.comment_type:
case "Comment":
c.content = frappe.utils.markdown(c.content)
docinfo.comments.append(c)
case "Shared" | "Unshared":
docinfo.shared.append(c)
case "Assignment Completed" | "Assigned":
docinfo.assignment_logs.append(c)
case "Attachment" | "Attachment Removed":
docinfo.attachment_logs.append(c)
case "Info" | "Edit" | "Label":
docinfo.info_logs.append(c)
case "Like":
docinfo.like_logs.append(c)
case "Workflow":
docinfo.workflow_logs.append(c)
return comments
@ -192,7 +189,9 @@ def get_attachments(dt, dn):
)
def get_versions(doc):
def get_versions(doc: "Document") -> list[dict]:
if not doc.meta.track_changes:
return []
return frappe.get_all(
"Version",
filters=dict(ref_doctype=doc.doctype, docname=doc.name),
@ -362,32 +361,29 @@ def run_onload(doc):
doc.run_method("onload")
def get_view_logs(doctype, docname):
def get_view_logs(doc: "Document") -> list[dict]:
"""get and return the latest view logs if available"""
logs = []
if getattr(frappe.get_meta(doctype), "track_views", None):
view_logs = frappe.get_all(
"View Log",
filters={
"reference_doctype": doctype,
"reference_name": docname,
},
fields=["name", "creation", "owner"],
order_by="creation desc",
)
if not doc.meta.track_views:
return []
if view_logs:
logs = view_logs
return logs
return frappe.get_all(
"View Log",
filters={
"reference_doctype": doc.doctype,
"reference_name": doc.name,
},
fields=["name", "creation", "owner"],
order_by="creation desc",
)
def get_tags(doctype, name):
tags = [
tag.tag
for tag in frappe.get_all(
"Tag Link", filters={"document_type": doctype, "document_name": name}, fields=["tag"]
)
]
def get_tags(doctype: str, name: str) -> str:
tags = frappe.get_all(
"Tag Link",
filters={"document_type": doctype, "document_name": name},
fields=["tag"],
pluck="tag",
)
return ",".join(tags)
@ -478,17 +474,20 @@ def send_link_titles(link_titles):
def update_user_info(docinfo):
for d in docinfo.communications:
frappe.utils.add_user_info(d.sender, docinfo.user_info)
users = set()
for d in docinfo.shared:
frappe.utils.add_user_info(d.user, docinfo.user_info)
users.update(d.sender for d in docinfo.communications)
users.update(d.user for d in docinfo.shared)
users.update(d.owner for d in docinfo.assignments)
users.update(d.owner for d in docinfo.views)
users.update(d.owner for d in docinfo.workflow_logs)
users.update(d.owner for d in docinfo.like_logs)
users.update(d.owner for d in docinfo.info_logs)
users.update(d.owner for d in docinfo.attachment_logs)
users.update(d.owner for d in docinfo.assignment_logs)
users.update(d.owner for d in docinfo.comments)
for d in docinfo.assignments:
frappe.utils.add_user_info(d.owner, docinfo.user_info)
for d in docinfo.views:
frappe.utils.add_user_info(d.owner, docinfo.user_info)
frappe.utils.add_user_info(users, docinfo.user_info)
@frappe.whitelist()

View file

@ -8,7 +8,7 @@
"include_name_field": 0,
"is_standard": 1,
"list_name": "",
"modified": "2023-08-24 11:01:18.688875",
"modified": "2023-05-24 12:43:43.741781",
"modified_by": "Administrator",
"module": "Desk",
"name": "Main Workspace Tour",
@ -22,7 +22,7 @@
"steps": [
{
"description": "This is Awesomebar, it helps you to navigate anywhere in the system, find documents, reports, settings, create new records and many more things.",
"element_selector": "#navbar-search[aria-expanded=\"true\"]",
"element_selector": "#navbar-search",
"fieldtype": "0",
"has_next_condition": 0,
"hide_buttons": 0,

View file

@ -25,8 +25,7 @@ def get_report_doc(report_name):
if doc.report_type == "Custom Report":
custom_report_doc = doc
reference_report = custom_report_doc.reference_report
doc = frappe.get_doc("Report", reference_report)
doc = get_reference_report(doc)
doc.custom_report = report_name
if custom_report_doc.json:
data = json.loads(custom_report_doc.json)
@ -172,9 +171,17 @@ def get_script(report_name):
"html_format": html_format,
"execution_time": frappe.cache.hget("report_execution_time", report_name) or 0,
"filters": report.filters,
"custom_report_name": report.name if report.get("is_custom_report") else None,
}
def get_reference_report(report):
if report.report_type != "Custom Report":
return report
reference_report = frappe.get_doc("Report", report.reference_report)
return get_reference_report(reference_report)
@frappe.whitelist()
@frappe.read_only()
def run(

View file

@ -135,6 +135,7 @@ class SiteMigration:
sync_customizations()
sync_languages()
flush_deferred_inserts()
frappe.model.sync.remove_orphan_doctypes()
frappe.get_single("Portal Settings").sync_menu()
frappe.get_single("Installed Applications").update_versions()

View file

@ -216,7 +216,7 @@ class Document(BaseDocument):
def check_permission(self, permtype="read", permlevel=None):
"""Raise `frappe.PermissionError` if not permitted"""
if not self.has_permission(permtype):
self.raise_no_permission_to(permlevel or permtype)
self.raise_no_permission_to(permtype)
def has_permission(self, permtype="read") -> bool:
"""

View file

@ -7,6 +7,8 @@
import os
import frappe
from frappe.cache_manager import clear_controller_cache
from frappe.model.base_document import get_controller
from frappe.modules.import_file import import_file_by_path
from frappe.modules.patch_handler import _patch_mode
from frappe.utils import update_progress_bar
@ -135,3 +137,37 @@ def get_doc_files(files, start_path):
files.append(doc_path)
return files
def remove_orphan_doctypes():
"""Find and remove any orphaned doctypes.
These are doctypes for which code and schema file is
deleted but entry is present in DocType table.
Note: Deleting the entry doesn't delete any data.
So this is supposed to be non-destrictive operation.
"""
doctype_names = frappe.get_all("DocType", {"custom": 0}, pluck="name")
orphan_doctypes = []
clear_controller_cache()
for doctype in doctype_names:
try:
get_controller(doctype=doctype)
except ImportError:
orphan_doctypes.append(doctype)
except Exception:
continue
if not orphan_doctypes:
return
print(f"Orphaned DocType(s) found: {', '.join(orphan_doctypes)}")
for i, name in enumerate(orphan_doctypes):
frappe.delete_doc("DocType", name, force=True, ignore_missing=True)
update_progress_bar("Deleting orphaned DocTypes", i, len(orphan_doctypes))
frappe.db.commit()
print()

View file

@ -228,6 +228,5 @@ frappe.patches.v15_0.remove_background_jobs_from_dropdown
frappe.desk.doctype.form_tour.patches.introduce_ui_tours
execute:frappe.delete_doc_if_exists("Workspace", "Customization")
execute:frappe.db.set_single_value("Document Naming Settings", "default_amend_naming", "Amend Counter")
execute:frappe.delete_doc_if_exists("DocType", "Error Snapshot")
frappe.patches.v15_0.move_event_cancelled_to_status
frappe.patches.v15_0.set_file_type

View file

@ -546,13 +546,23 @@ def can_export(doctype, raise_exception=False):
return has_access
def update_permission_property(doctype, role, permlevel, ptype, value=None, validate=True):
def update_permission_property(
doctype,
role,
permlevel,
ptype,
value=None,
validate=True,
if_owner=0,
):
"""Update a property in Custom Perm"""
from frappe.core.doctype.doctype.doctype import validate_permissions_for_doctype
out = setup_custom_perms(doctype)
name = frappe.db.get_value("Custom DocPerm", dict(parent=doctype, role=role, permlevel=permlevel))
name = frappe.db.get_value(
"Custom DocPerm", dict(parent=doctype, role=role, permlevel=permlevel, if_owner=if_owner)
)
table = DocType("Custom DocPerm")
frappe.qb.update(table).set(ptype, value).where(table.name == name).run()
@ -579,6 +589,12 @@ def add_permission(doctype, role, permlevel=0, ptype=None):
if frappe.db.get_value(
"Custom DocPerm", dict(parent=doctype, role=role, permlevel=permlevel, if_owner=0)
):
frappe.msgprint(
_("Rule for this doctype, role, permlevel and if-owner combination already exists.").format(
doctype,
),
alert=True,
)
return
if not ptype:

View file

@ -120,6 +120,12 @@
<path d="M3.14886 8.08422L5.23297 6.00012L3.14886 3.91602" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-external-link">
<path d="M9.75003 7.83333V9C9.75003 9.82843 9.07846 10.5 8.25003 10.5H3.25C2.42157 10.5 1.75 9.82843 1.75 9V4C1.75 3.17158 2.42151 2.50001 3.24993 2.50001C3.62327 2.5 4.02808 2.5 4.4167 2.5" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.75 1.5H10.25V4.5" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.75 5L9.75 2" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" fill="#112B42" id="icon-up">
<path d="M3 5h6L6 2 3 5z"></path>
<path opacity=".5" d="M6 10l3-3H3l3 3z"></path>
@ -302,7 +308,7 @@
<path stroke="#E24C4C" stroke-linejoin="round" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M13.2 14.4v8.571"></path>
</symbol>
<symbol viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" id="icon-external-link">
<symbol viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" id="icon-pen">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.348 3.207a1 1 0 0 1 1.415 0l1.03 1.03a1 1 0 0 1 0 1.415l-6.626 6.626L2.5 13.5l1.222-3.667 6.626-6.626z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"></path>
</symbol>

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 114 KiB

View file

@ -1,22 +1,40 @@
<template>
<div class="tree-node" :class="{ opened: node.open }">
<span
ref="reference"
class="tree-link"
@click="emit('node-click', node)"
:class="{ active: node.value === selected_node.value }"
:disabled="node.fetching"
@mouseover="onMouseover"
@mouseleave="onMouseleave"
>
<div v-html="icon"></div>
<a class="tree-label">{{ node.label }}</a>
<!-- Icon open File record in new tab -->
<a
v-if="node.is_leaf"
:href="open_file(node.value)"
:disabled="node.fetching"
target="_blank"
class="file-doc-link ml-2"
v-html="frappe.utils.icon('external-link', 'sm')"
@click.stop
/>
</span>
<div v-if="node.file_url && frappe.utils.is_image_file(node.file_url)">
<div v-show="isOpen" class="popover" ref="popover" role="tooltip">
<img :src="node.file_url" />
</div>
</div>
<ul class="tree-children" v-show="node.open">
<TreeNode
v-for="n in node.children"
:key="n.value"
:node="n"
:selected_node="selected_node"
@node-click="n => emit('node-click', n)"
@load-more="n => emit('load-more', n)"
@node-click="(n) => emit('node-click', n)"
@load-more="(n) => emit('load-more', n)"
/>
<button
class="btn btn-xs btn-load-more"
@ -32,7 +50,8 @@
<script setup>
import TreeNode from "./TreeNode.vue";
import { computed } from "vue";
import { createPopper } from "@popperjs/core";
import { ref, computed } from "vue";
// props
const props = defineProps({
@ -50,7 +69,7 @@ let icon = computed(() => {
open: frappe.utils.icon("folder-open", "md"),
closed: frappe.utils.icon("folder-normal", "md"),
leaf: frappe.utils.icon("primitive-dot", "xs"),
search: frappe.utils.icon("search")
search: frappe.utils.icon("search"),
};
if (props.node.by_search) return icons.search;
@ -59,6 +78,52 @@ let icon = computed(() => {
return icons.closed;
});
let open_file = (filename) => {
return frappe.utils.get_form_link("File", filename);
};
const reference = ref(null);
const popover = ref(null);
let isOpen = ref(false);
let popper = ref(null);
function setupPopper() {
if (!popper.value) {
popper.value = createPopper(reference.value, popover.value, {
placement: "top",
modifiers: [
{
name: "offset",
options: {
offset: [0, 4],
},
},
],
});
} else {
popper.value.update();
}
}
let hoverTimer = null;
let leaveTimer = null;
function onMouseover() {
leaveTimer && clearTimeout(leaveTimer) && (leaveTimer = null);
hoverTimer && clearTimeout(hoverTimer);
hoverTimer = setTimeout(() => {
isOpen.value = true;
setupPopper();
}, 800);
}
function onMouseleave() {
hoverTimer && clearTimeout(hoverTimer) && (hoverTimer = null);
leaveTimer && clearTimeout(leaveTimer);
leaveTimer = setTimeout(() => {
isOpen.value = false;
}, 100);
}
</script>
<style scoped>
@ -66,4 +131,7 @@ let icon = computed(() => {
margin-left: 1.6rem;
margin-top: 0.5rem;
}
.popover {
padding: 10px;
}
</style>

View file

@ -1,24 +1,44 @@
<div class="clearfix"></div>
{% for(var i=0, l=addr_list.length; i<l; i++) { %}
<div class="address-box">
<p class="h6">
{%= i+1 %}. {%= addr_list[i].address_title %}{% if(addr_list[i].address_type!="Other") { %}
<span class="text-muted">({%= __(addr_list[i].address_type) %})</span>{% } %}
{% if(addr_list[i].is_primary_address) { %}
<span class="text-muted">({%= __("Primary") %})</span>{% } %}
{% if(addr_list[i].is_shipping_address) { %}
<span class="text-muted">({%= __("Shipping") %})</span>{% } %}
{% if(addr_list[i].disabled) { %}
<span class="text-muted">({%= __("Disabled") %})</span>{% } %}
{% for (const addr of addr_list) { %}
<div class="address-box">
<a
href="{%= frappe.utils.get_form_link('Address', addr.name) %}"
class="btn btn-xs btn-default edit-btn"
title="{%= __('Edit') %}"
>
<svg class="icon icon-xs">
<use href="#icon-edit"></use>
</svg>
</a>
<p class="h6 flex flex-wrap">
<span>{%= addr.address_title %}</span>
{% if (addr.address_type !== "Other") { %}
&nbsp;&#183;&nbsp;
<span class="text-muted">{%= __(addr.address_type) %}</span>
{% } %}
{% if (addr.is_primary_address) { %}
&nbsp;&#183;&nbsp;
<span class="text-muted">{%= __("Primary Address") %}</span>
{% } %}
{% if (addr.is_shipping_address) { %}
&nbsp;&#183;&nbsp;
<span class="text-muted">{%= __("Shipping Address") %}</span>
{% } %}
{% if (addr.disabled) { %}
&nbsp;&#183;&nbsp;
<span class="text-muted">{%= __("Disabled") %}</span>
{% } %}
</p>
<p>{%= addr.display %}</p>
</div>
{% } %}
<a href="/app/Form/Address/{%= encodeURIComponent(addr_list[i].name) %}" class="btn btn-default btn-xs pull-right"
style="margin-top:-3px; margin-right: -5px;">
{%= __("Edit") %}</a>
</p>
<p>{%= addr_list[i].display %}</p>
</div>
{% if (!addr_list.length) { %}
<p class="text-muted small">{%= __("No address added yet.") %}</p>
{% } %}
{% if(!addr_list.length) { %}
<p class="text-muted small">{%= __("No address added yet.") %}</p>
{% } %}
<p><button class="btn btn-xs btn-default btn-address">{{ __("New Address") }}</button></p>
<p>
<button class="btn btn-xs btn-default btn-address">
{{ __("New Address") }}
</button>
</p>

View file

@ -1,42 +1,74 @@
<div class="clearfix"></div>
{% for(const contact of contact_list) { %}
{% for (const contact of contact_list) { %}
<div class="address-box">
<p class="h6 flex align-center">
{%= contact.first_name %} {%= contact.last_name %}
{% if(contact.is_primary_contact) { %}
<span class="text-muted">&nbsp;({%= __("Primary") %})</span>
<a
href="{%= frappe.utils.get_form_link('Contact', contact.name) %}"
class="btn btn-xs btn-default edit-btn"
title="{%= __('Edit') %}"
>
<svg class="icon icon-xs">
<use href="#icon-edit"></use>
</svg>
</a>
<p class="h6 flex flex-wrap">
<span>{%= contact.first_name %} {%= contact.last_name %}</span>
{% if (contact.is_primary_contact) { %}
&nbsp;&#183;&nbsp;
<span class="text-muted">{%= __("Primary Contact") %}</span>
{% } %}
{% if(contact.designation){ %}
<span class="text-muted">&ndash; {%= contact.designation %}</span>
{% if (contact.is_billing_contact) { %}
&nbsp;&#183;&nbsp;
<span class="text-muted">{%= __("Billing Contact") %}</span>
{% } %}
{% if (contact.designation){ %}
&nbsp;&#183;&nbsp;
<span class="text-muted"> {%= contact.designation %}</span>
{% } %}
<a href="/app/Form/Contact/{%= encodeURIComponent(contact.name) %}"
class="btn btn-xs btn-default ml-auto">
{%= __("Edit") %}
</a>
</p>
{% if (contact.phone || contact.mobile_no || contact.phone_nos.length > 0) { %}
<p>
{% if(contact.phone) { %}
<a href="tel:{%= frappe.utils.escape_html(contact.phone) %}">{%= frappe.utils.escape_html(contact.phone) %}</a> &#183; <span class="text-muted">{%= __("Primary Phone") %}</span><br>
{% if (contact.phone) { %}
<a href="tel:{%= frappe.utils.escape_html(contact.phone) %}">
{%= frappe.utils.escape_html(contact.phone) %}
</a>
&#183;
<span class="text-muted">{%= __("Primary Phone") %}</span>
<br>
{% endif %}
{% if(contact.mobile_no) { %}
<a href="tel:{%= frappe.utils.escape_html(contact.mobile_no) %}">{%= frappe.utils.escape_html(contact.mobile_no) %}</a> &#183; <span class="text-muted">{%= __("Primary Mobile") %}</span><br>
{% if (contact.mobile_no) { %}
<a href="tel:{%= frappe.utils.escape_html(contact.mobile_no) %}">
{%= frappe.utils.escape_html(contact.mobile_no) %}
</a>
&#183;
<span class="text-muted">{%= __("Primary Mobile") %}</span>
<br>
{% endif %}
{% if(contact.phone_nos) { %}
{% for(const phone_no of contact.phone_nos) { %}
<a href="tel:{%= frappe.utils.escape_html(phone_no.phone) %}">{%= frappe.utils.escape_html(phone_no.phone) %}</a><br>
{% if (contact.phone_nos) { %}
{% for (const phone_no of contact.phone_nos) { %}
<a href="tel:{%= frappe.utils.escape_html(phone_no.phone) %}">
{%= frappe.utils.escape_html(phone_no.phone) %}
</a>
<br>
{% } %}
{% endif %}
</p>
{% endif %}
{% if (contact.email_id || contact.email_ids.length > 0) { %}
<p>
{% if(contact.email_id) { %}
<a href="mailto:{%= frappe.utils.escape_html(contact.email_id) %}">{%= frappe.utils.escape_html(contact.email_id) %}</a> &#183; <span class="text-muted">{%= __("Primary Email") %}</span><br>
{% if (contact.email_id) { %}
<a href="mailto:{%= frappe.utils.escape_html(contact.email_id) %}">
{%= frappe.utils.escape_html(contact.email_id) %}
</a>
&#183;
<span class="text-muted">{%= __("Primary Email") %}</span>
<br>
{% endif %}
{% if(contact.email_ids) { %}
{% for(const email_id of contact.email_ids) { %}
<a href="mailto:{%= frappe.utils.escape_html(email_id.email_id) %}">{%= frappe.utils.escape_html(email_id.email_id) %}</a><br>
{% if (contact.email_ids) { %}
{% for (const email_id of contact.email_ids) { %}
<a href="mailto:{%= frappe.utils.escape_html(email_id.email_id) %}">
{%= frappe.utils.escape_html(email_id.email_id) %}
</a>
<br>
{% } %}
{% endif %}
</p>
@ -48,9 +80,13 @@
{% endif %}
</div>
{% } %}
{% if(!contact_list.length) { %}
<p class="text-muted small">{%= __("No contacts added yet.") %}</p>
{% if (!contact_list.length) { %}
<p class="text-muted small">{%= __("No contacts added yet.") %}</p>
{% } %}
<p><button class="btn btn-xs btn-default btn-contact">
{{ __("New Contact") }}</button>
<p>
<button class="btn btn-xs btn-default btn-contact">
{{ __("New Contact") }}
</button>
</p>

View file

@ -378,7 +378,8 @@ frappe.ui.form.Toolbar = class Toolbar {
function () {
me.frm.copy_doc();
},
true
true,
"Shift+D"
);
}

View file

@ -634,7 +634,10 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
<span class="level-item list-liked-by-me hidden-xs">
<span title="${__("Likes")}">${frappe.utils.icon("es-solid-heart", "sm", "like-icon")}</span>
</span>
<span class="level-item">${__(subject_field.label)}</span>
<span class="level-item" data-sort-by="${subject_field.fieldname}"
title="${__("Click to sort by {0}", [subject_field.label])}">
${__(subject_field.label)}
</span>
`;
const $columns = this.columns
.map((col) => {
@ -645,15 +648,21 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
frappe.model.is_numeric_field(col.df) ? "text-right" : "",
].join(" ");
return `
<div class="${classes}">
${
col.type === "Subject"
? subject_html
: `
<span>${__((col.df && col.df.label) || col.type)}</span>`
}
</div>
let html = "";
if (col.type === "Subject") {
html = subject_html;
} else {
const fieldname = col.df?.fieldname;
const attrs = fieldname
? ` data-sort-by="${fieldname}"
title="${__("Click to sort by {0}", [col.df?.label])}"`
: "";
html = `<span ${attrs}>
${__(col.df?.label || col.type)}
</span>`;
}
return `<div class="${classes}">${html}</div>
`;
})
.join("");
@ -1062,6 +1071,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
setup_events() {
this.setup_filterable();
this.setup_sort_by();
this.setup_list_click();
this.setup_drag_click();
this.setup_tag_event();
@ -1203,6 +1213,20 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
});
}
setup_sort_by() {
this.$result.on("click", "[data-sort-by]", (e) => {
const sort_by = e.currentTarget.getAttribute("data-sort-by");
if (!sort_by) return;
let sort_order = "asc"; // always start with asc
if (this.sort_by === sort_by) {
// unless it's the same field, then toggle
sort_order = this.sort_order === "asc" ? "desc" : "asc";
}
this.sort_selector.set_value(sort_by, sort_order);
this.on_sort_change(sort_by, sort_order);
});
}
setup_list_click() {
this.$result.on("click", ".list-row, .image-view-header, .file-header", (e) => {
const $target = $(e.target);

View file

@ -23,26 +23,35 @@ frappe.ui.SortSelector = class SortSelector {
// order
this.wrapper.find(".btn-order").on("click", function () {
let btn = $(this);
const order = $(this).attr("data-value") === "desc" ? "asc" : "desc";
const title =
$(this).attr("data-value") === "desc" ? __("ascending") : __("descending");
btn.attr("data-value", order);
btn.attr("title", title);
me.sort_order = order;
const icon_name = order === "asc" ? "sort-ascending" : "sort-descending";
btn.find(".sort-order").html(frappe.utils.icon(icon_name, "sm"));
me.set_value(me.sort_by, order);
(me.onchange || me.change)(me.sort_by, me.sort_order);
});
// select field
this.wrapper.find(".dropdown-menu a.option").on("click", function () {
me.sort_by = $(this).attr("data-value");
me.wrapper.find(".dropdown-text").html($(this).html());
me.set_value($(this).attr("data-value"), me.sort_order);
(me.onchange || me.change)(me.sort_by, me.sort_order);
});
}
set_value(sort_by, sort_order) {
const $btn = this.wrapper.find(".btn-order");
const $icon = $btn.find(".sort-order");
const $text = this.wrapper.find(".dropdown-text");
if (this.sort_by !== sort_by) {
this.sort_by = sort_by;
$text.html(__(this.get_label(sort_by)));
}
if (this.sort_order !== sort_order) {
this.sort_order = sort_order;
const title = sort_order === "desc" ? __("ascending") : __("descending");
const icon_name = sort_order === "asc" ? "sort-ascending" : "sort-descending";
$btn.attr("data-value", sort_order);
$btn.attr("title", title);
$icon.html(frappe.utils.icon(icon_name, "sm"));
}
}
prepare_args() {
var me = this;
if (!this.args) {

View file

@ -8,12 +8,7 @@ if (!window.frappe) window.frappe = {};
function flt(v, decimals, number_format, rounding_method) {
if (v == null || v == "") return 0;
if (!(typeof v === "number" || String(parseFloat(v)) == v)) {
// cases in which this block should not run
// 1. 'v' is already a number
// 2. v is already parsed but in string form
// if (typeof v !== "number") {
if (typeof v !== "number") {
v = v + "";
// strip currency symbol if exists
@ -29,7 +24,6 @@ function flt(v, decimals, number_format, rounding_method) {
if (isNaN(v)) v = 0;
}
v = parseFloat(v);
if (decimals != null) return _round(v, decimals, rounding_method);
return v;
}

View file

@ -419,7 +419,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
.then((settings) => {
frappe.dom.eval(settings.script || "");
frappe.after_ajax(() => {
this.report_settings = this.get_local_report_settings();
this.report_settings = this.get_local_report_settings(
settings.custom_report_name
);
this.report_settings.html_format = settings.html_format;
this.report_settings.execution_time = settings.execution_time || 0;
frappe.query_reports[this.report_name] = this.report_settings;
@ -437,10 +439,12 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
});
}
get_local_report_settings() {
get_local_report_settings(custom_report_name) {
let report_script_name =
this.report_doc.report_type === "Custom Report"
? this.report_doc.reference_report
? custom_report_name
? custom_report_name
: this.report_doc.reference_report
: this.report_name;
return frappe.query_reports[report_script_name] || {};
}

View file

@ -11,11 +11,11 @@ frappe.ui.OnboardingTour = class OnboardingTour {
allowClose: false,
padding: 10,
overlayClickNext: false,
keyboardControl: true,
keyboardControl: false,
nextBtnText: __("Next"),
prevBtnText: __("Previous"),
doneBtnText: __("Done"),
closeBtnText: __("Close"),
closeBtnText: __("Skip"),
opacity: 0.5,
onHighlighted: (step) => {
frappe.ui.next_form_tour = step.options.step_info?.next_form_tour;

View file

@ -125,14 +125,8 @@ export const useStore = defineStore("workflow-builder-store", () => {
let docfield = "Workflow Document State";
let df = frappe.model.get_new_doc(docfield);
df.name = frappe.utils.get_random(8);
df.state = data.state;
Object.assign(df, data);
df.doc_status = doc_status_map[data.doc_status];
df.allow_edit = data.allow_edit;
df.update_field = data.update_field;
df.update_value = data.update_value;
df.is_optional_state = data.is_optional_state;
df.next_action_email_template = data.next_action_email_template;
df.message = data.message;
return df;
}
@ -146,14 +140,11 @@ export const useStore = defineStore("workflow-builder-store", () => {
return states;
}
function get_transition_df({ state, action, next_state, allowed }) {
function get_transition_df(data) {
let docfield = "Workflow Transition";
let df = frappe.model.get_new_doc(docfield);
df.name = frappe.utils.get_random(8);
df.state = state;
df.action = action;
df.next_state = next_state;
df.allowed = allowed;
Object.assign(df, data);
return df;
}
@ -180,10 +171,9 @@ export const useStore = defineStore("workflow-builder-store", () => {
}
transitions.push(
get_transition_df({
...action.data,
state: action.data.from,
action: action.data.action,
next_state: action.data.to,
allowed: action.data.allowed,
})
);
});

View file

@ -150,9 +150,19 @@ select.form-control {
border-radius: var(--border-radius);
@include get_textstyle("sm", "regular");
word-wrap: break-word;
position: relative;
p:last-child {
margin-bottom: 0;
}
.edit-btn {
position: absolute;
top: 5px;
right: 5px;
display: flex;
justify-content: center;
align-items: center;
padding: var(--padding-sm);
}
}
.action-btn {
position: absolute;

View file

@ -178,6 +178,11 @@
a {
color: var(--text-muted);
}
& > [data-sort-by]:hover {
cursor: pointer;
text-decoration: underline;
}
}
$level-margin-right: 8px;

View file

@ -58,6 +58,19 @@
}
}
.tree-link .file-doc-link {
opacity: 0;
}
.tree-link:hover .file-doc-link {
opacity: 0.4;
}
.tree-link .file-doc-link:hover {
transform: scale(1.1);
opacity: 0.5;
}
.tree-hover {
background-color: var(--highlight-color);
min-height: 25px;

View file

@ -477,9 +477,9 @@ class TestPermissions(FrappeTestCase):
# check if user is not granted access if the user is not the owner of the doc
# Blogger has only read access on the blog post unless he is the owner of the blog
update("Blog Post", "Blogger", 0, "if_owner", 1)
update("Blog Post", "Blogger", 0, "read", 1)
update("Blog Post", "Blogger", 0, "write", 1)
update("Blog Post", "Blogger", 0, "delete", 1)
update("Blog Post", "Blogger", 0, "read", 1, 1)
update("Blog Post", "Blogger", 0, "write", 1, 1)
update("Blog Post", "Blogger", 0, "delete", 1, 1)
# currently test2 user has not created any document
# still he should be able to do get_list query which should
@ -588,9 +588,9 @@ class TestPermissions(FrappeTestCase):
def test_if_owner_permission_on_delete(self):
update("Blog Post", "Blogger", 0, "if_owner", 1)
update("Blog Post", "Blogger", 0, "read", 1)
update("Blog Post", "Blogger", 0, "write", 1)
update("Blog Post", "Blogger", 0, "delete", 1)
update("Blog Post", "Blogger", 0, "read", 1, 1)
update("Blog Post", "Blogger", 0, "write", 1, 1)
update("Blog Post", "Blogger", 0, "delete", 1, 1)
# Remove delete perm
update("Blog Post", "Website Manager", 0, "delete", 0)
@ -627,7 +627,7 @@ class TestPermissions(FrappeTestCase):
frappe.set_user("test2@example.com")
frappe.delete_doc("Blog Post", "-test-blog-post-title-new-1")
update("Blog Post", "Website Manager", 0, "delete", 1)
update("Blog Post", "Website Manager", 0, "delete", 1, 1)
def test_clear_user_permissions(self):
current_user = frappe.session.user

View file

@ -8,7 +8,7 @@ from contextlib import contextmanager
from unittest.mock import patch
import frappe
from frappe.model.base_document import BaseDocument
from frappe.model.base_document import BaseDocument, get_controller
from frappe.utils import cint
datetime_like_types = (datetime.datetime, datetime.date, datetime.time, datetime.timedelta)
@ -253,3 +253,21 @@ def patch_hooks(overridden_hoooks):
with patch.object(frappe, "get_hooks", patched_hooks):
yield
def check_orpahned_doctypes():
"""Check that all doctypes in DB actually exist after patch test"""
doctypes = frappe.get_all("DocType", {"custom": 0}, pluck="name")
orpahned_doctypes = []
for doctype in doctypes:
try:
get_controller(doctype)
except ImportError:
orpahned_doctypes.append(doctype)
if orpahned_doctypes:
frappe.throw(
"Following doctypes exist in DB without controller.\n {}".format("\n".join(orpahned_doctypes))
)

View file

@ -1384,7 +1384,7 @@ Is Globally Pinned,Wird global gepinnt,
Is Home Folder,Ist Ordner für Startseite,
Is Mandatory Field,Ist Pflichtfeld,
Is Pinned,Ist angeheftet,
Is Primary Contact,Ist primärer Ansprechpartner,
Is Primary Contact,Ist Hauptkontakt,
Is Private,Ist Privat,
Is Published Field,Ist Veröffentlicht Feld,
Is Published Field must be a valid fieldname,Ist Veröffentlicht Feld muss eine gültige Feldname sein,
@ -1940,6 +1940,11 @@ Preview Message,Vorschau Nachricht,
Previous,Vorhergehende,
Previous Hash,Vorheriger Hash,
Primary Color,Primärfarbe,
Primary Address,Hauptadresse,
Primary Contact,Hauptkontakt,
Primary Email,Haupt-E-Mail,
Primary Mobile,Haupt-Mobiltelefon,
Primary Phone,Haupttelefon,
Print Documents,Dokumente drucken,
Print Format Builder,Programm zum Erstellen von Druckformaten,
Print Format Help,Hilfe zu Druckformaten,
@ -4851,6 +4856,7 @@ Anonymous,Anonym,
Author,Autor,
Basic,Grundeinkommen,
Billing,Abrechnung,
Billing Contact,Abrechnungskontakt,
Contact Details,Kontakt-Details,
Datetime,Datum und Uhrzeit,
Enable,ermöglichen,
@ -4879,6 +4885,7 @@ Saved,Gespeichert,
Series {0} already used in {1},Serie {0} bereits verwendet in {1},
Set as Default,Als Standard festlegen,
Shipping,Versand,
Shipping Address,Lieferadresse,
Standard,Standard,
Test,Test,
Traceback,Zurück verfolgen,

1 A4 A4
1384 Is Home Folder Ist Ordner für Startseite
1385 Is Mandatory Field Ist Pflichtfeld
1386 Is Pinned Ist angeheftet
1387 Is Primary Contact Ist primärer Ansprechpartner Ist Hauptkontakt
1388 Is Private Ist Privat
1389 Is Published Field Ist Veröffentlicht Feld
1390 Is Published Field must be a valid fieldname Ist Veröffentlicht Feld muss eine gültige Feldname sein
1940 Previous Vorhergehende
1941 Previous Hash Vorheriger Hash
1942 Primary Color Primärfarbe
1943 Primary Address Hauptadresse
1944 Primary Contact Hauptkontakt
1945 Primary Email Haupt-E-Mail
1946 Primary Mobile Haupt-Mobiltelefon
1947 Primary Phone Haupttelefon
1948 Print Documents Dokumente drucken
1949 Print Format Builder Programm zum Erstellen von Druckformaten
1950 Print Format Help Hilfe zu Druckformaten
4856 Author Autor
4857 Basic Grundeinkommen
4858 Billing Abrechnung
4859 Billing Contact Abrechnungskontakt
4860 Contact Details Kontakt-Details
4861 Datetime Datum und Uhrzeit
4862 Enable ermöglichen
4885 Series {0} already used in {1} Serie {0} bereits verwendet in {1}
4886 Set as Default Als Standard festlegen
4887 Shipping Versand
4888 Shipping Address Lieferadresse
4889 Standard Standard
4890 Test Test
4891 Traceback Zurück verfolgen

View file

@ -206,3 +206,4 @@ class TypeExporter:
# Ideally this should be longest common substring but I don't l33tc0de.
# If someone really needs it, add support via hooks.
self.indent = " " * 4
break

View file

@ -21,7 +21,7 @@ from collections.abc import (
)
from email.header import decode_header, make_header
from email.utils import formataddr, parseaddr
from typing import Any, Literal
from typing import Any, Literal, TypedDict
from urllib.parse import quote, urlparse
from redis.exceptions import ConnectionError
@ -1082,15 +1082,36 @@ def dictify(arg):
return arg
def add_user_info(user, user_info):
if user not in user_info:
info = (
frappe.db.get_value(
"User", user, ["full_name", "user_image", "name", "email", "time_zone"], as_dict=True
)
or frappe._dict()
)
user_info[user] = frappe._dict(
class _UserInfo(TypedDict):
fullname: str
image: str
name: str
email: str
time_zone: str
def add_user_info(user: str | list[str] | set[str], user_info: dict[str, _UserInfo]) -> None:
if not user:
return
if isinstance(user, str):
user = [user]
missing_users = [u for u in user if u not in user_info]
if not missing_users:
return
for missing_user in missing_users:
user_info[missing_user] = frappe._dict()
missing_info = frappe.get_all(
"User",
{"name": ("in", missing_users)},
["full_name", "user_image", "name", "email", "time_zone"],
)
for info in missing_info:
user_info[info.name].update(
fullname=info.full_name or user,
image=info.user_image,
name=user,

View file

@ -69,7 +69,7 @@ def safe_exec(script, _globals=None, _locals=None, restrict_commit_rollback=Fals
if not is_safe_exec_enabled():
msg = _("Server Scripts are disabled. Please enable server scripts from bench configuration.")
docs_cta = _("Read the documentation to know")
docs_cta = _("Read the documentation to know more")
msg += f"<br><a href='https://frappeframework.com/docs/user/en/desk/scripting/server-script'>{docs_cta}</a>"
frappe.throw(msg, ServerScriptNotEnabled, title="Server Scripts Disabled")

View file

@ -49,6 +49,7 @@ class PortalSettings(Document):
dirty = True
if dirty:
self.remove_deleted_doctype_items()
self.save()
def on_update(self):
@ -65,3 +66,9 @@ class PortalSettings(Document):
# clears role based home pages
frappe.clear_cache()
def remove_deleted_doctype_items(self):
existing_doctypes = set(frappe.get_list("DocType", pluck="name"))
for menu_item in list(self.get("menu")):
if menu_item.reference_doctype not in existing_doctypes:
self.remove(menu_item)

View file

@ -17,10 +17,11 @@ from frappe.website.utils import can_cache, get_home_page
class PathResolver:
__slots__ = ("path",)
__slots__ = ("path", "http_status_code")
def __init__(self, path):
def __init__(self, path, http_status_code=None):
self.path = path.strip("/ ")
self.http_status_code = http_status_code
def resolve(self):
"""Returns endpoint and a renderer instance that can render the endpoint"""
@ -41,7 +42,7 @@ class PathResolver:
# WARN: Hardcoded for better performance
if endpoint == "app":
return endpoint, TemplatePage(endpoint, 200)
return endpoint, TemplatePage(endpoint, self.http_status_code)
custom_renderers = self.get_custom_page_renderers()
renderers = custom_renderers + [
@ -54,7 +55,7 @@ class PathResolver:
]
for renderer in renderers:
renderer_instance = renderer(endpoint, 200)
renderer_instance = renderer(endpoint, self.http_status_code)
if renderer_instance.can_render():
return endpoint, renderer_instance

View file

@ -13,7 +13,7 @@ def get_response(path=None, http_status_code=200):
endpoint = path
try:
path_resolver = PathResolver(path)
path_resolver = PathResolver(path, http_status_code)
endpoint, renderer_instance = path_resolver.resolve()
response = renderer_instance.render()
except frappe.Redirect:

View file

@ -20,6 +20,7 @@
},
"homepage": "https://frappeframework.com",
"dependencies": {
"@popperjs/core": "^2.11.2",
"@editorjs/editorjs": "^2.26.3",
"@frappe/esbuild-plugin-postcss2": "^0.1.3",
"@redis/client": "^1.5.8",

View file

@ -81,6 +81,11 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@popperjs/core@^2.11.2":
version "2.11.8"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
"@redis/client@^1.5.8":
version "1.5.8"
resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.5.8.tgz#a375ba7861825bd0d2dc512282b8bff7b98dbcb1"