chore: merge develop
This commit is contained in:
commit
9d3dd2a12a
16 changed files with 152 additions and 59 deletions
|
|
@ -253,7 +253,11 @@ class LoginManager:
|
|||
):
|
||||
return
|
||||
|
||||
clear_sessions(frappe.session.user, keep_current=True)
|
||||
clear_sessions(
|
||||
frappe.session.user,
|
||||
keep_current=True,
|
||||
force=frappe.session.user != "Administrator",
|
||||
)
|
||||
|
||||
def authenticate(self, user: str | None = None, pwd: str | None = None):
|
||||
from frappe.core.doctype.user.user import User
|
||||
|
|
|
|||
|
|
@ -248,7 +248,6 @@
|
|||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Note: Multiple sessions will be allowed in case of mobile device",
|
||||
"fieldname": "deny_multiple_sessions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow only one session per user"
|
||||
|
|
@ -790,7 +789,7 @@
|
|||
"icon": "fa fa-cog",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-02 18:13:45.430712",
|
||||
"modified": "2026-02-24 14:27:04.763075",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "System Settings",
|
||||
|
|
|
|||
|
|
@ -76,6 +76,18 @@ class Workspace(Document):
|
|||
|
||||
if self.public and not is_workspace_manager() and not disable_saving_as_public():
|
||||
frappe.throw(_("You need to be Workspace Manager to edit this document"))
|
||||
|
||||
if (
|
||||
not self.public
|
||||
and self.for_user
|
||||
and self.for_user != frappe.session.user
|
||||
and not is_workspace_manager()
|
||||
):
|
||||
frappe.throw(
|
||||
_("You are not allowed to edit this workspace"),
|
||||
frappe.PermissionError,
|
||||
)
|
||||
|
||||
if self.has_value_changed("title"):
|
||||
validate_route_conflict(self.doctype, self.title)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -514,7 +514,18 @@ def send_now(name: str | int, force_send: bool = False):
|
|||
@frappe.whitelist()
|
||||
def toggle_sending(enable: bool | int | str):
|
||||
frappe.only_for("System Manager")
|
||||
frappe.db.set_default("suspend_email_queue", 0 if sbool(enable) else 1)
|
||||
suspend_value = 0 if sbool(enable) else 1
|
||||
frappe.db.set_default("suspend_email_queue", suspend_value)
|
||||
|
||||
action = "Resumed" if suspend_value == 0 else "Suspended"
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Activity Log",
|
||||
"user": frappe.session.user,
|
||||
"status": "Success",
|
||||
"subject": f"Email Queue sending {action.lower()}",
|
||||
}
|
||||
).insert(ignore_permissions=True, ignore_links=True)
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@ import base64
|
|||
import datetime
|
||||
import hashlib
|
||||
import re
|
||||
from http import cookies
|
||||
from urllib.parse import unquote, urljoin, urlparse
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
from oauthlib.common import Request
|
||||
from oauthlib.openid import RequestValidator
|
||||
|
||||
import frappe
|
||||
from frappe.auth import LoginManager
|
||||
from frappe.integrations.doctype.oauth_client.oauth_client import OAuthClient
|
||||
from frappe.utils.data import cstr, get_system_timezone, now_datetime
|
||||
|
||||
|
||||
|
|
@ -73,13 +74,11 @@ class OAuthWebRequestValidator(RequestValidator):
|
|||
# Post-authorization
|
||||
|
||||
def save_authorization_code(self, client_id, code, request, *args, **kwargs):
|
||||
cookie_dict = get_cookie_dict_from_headers(request)
|
||||
|
||||
oac = frappe.new_doc("OAuth Authorization Code")
|
||||
oac.scopes = get_url_delimiter().join(request.scopes)
|
||||
oac.redirect_uri_bound_to_authorization_code = request.redirect_uri
|
||||
oac.client = client_id
|
||||
oac.user = unquote(cookie_dict["user_id"].value)
|
||||
oac.user = frappe.session.user
|
||||
oac.authorization_code = code["code"]
|
||||
|
||||
if request.nonce:
|
||||
|
|
@ -92,43 +91,32 @@ class OAuthWebRequestValidator(RequestValidator):
|
|||
oac.save(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
|
||||
def authenticate_client(self, request, *args, **kwargs):
|
||||
def authenticate_client(self, request: Request, *args, **kwargs) -> bool | None:
|
||||
"""
|
||||
Loads the client based on request parameters and sets in oauth request.
|
||||
Returns True on success, None on error.
|
||||
"""
|
||||
# Get ClientID in URL
|
||||
if request.client_id:
|
||||
oc = frappe.get_doc("OAuth Client", request.client_id)
|
||||
client_name = request.client_id
|
||||
else:
|
||||
# Extract token, instantiate OAuth Bearer Token and use clientid from there.
|
||||
if "refresh_token" in frappe.form_dict:
|
||||
oc = frappe.get_doc(
|
||||
"OAuth Client",
|
||||
frappe.db.get_value(
|
||||
"OAuth Bearer Token",
|
||||
{"refresh_token": frappe.form_dict["refresh_token"]},
|
||||
"client",
|
||||
),
|
||||
)
|
||||
token_filters = {"refresh_token": frappe.form_dict["refresh_token"]}
|
||||
elif "token" in frappe.form_dict:
|
||||
oc = frappe.get_doc(
|
||||
"OAuth Client",
|
||||
frappe.db.get_value("OAuth Bearer Token", frappe.form_dict["token"], "client"),
|
||||
)
|
||||
token_filters = {"name": frappe.form_dict["token"]}
|
||||
else:
|
||||
oc = frappe.get_doc(
|
||||
"OAuth Client",
|
||||
frappe.db.get_value(
|
||||
"OAuth Bearer Token",
|
||||
frappe.get_request_header("Authorization").split(" ")[1],
|
||||
"client",
|
||||
),
|
||||
)
|
||||
token_filters = {"name": frappe.get_request_header("Authorization").split(" ")[1]}
|
||||
|
||||
client_name = frappe.db.get_value("OAuth Bearer Token", filters=token_filters, fieldname="client")
|
||||
|
||||
oc: OAuthClient = frappe.get_doc("OAuth Client", client_name)
|
||||
try:
|
||||
request.client = request.client or oc.as_dict()
|
||||
except Exception as e:
|
||||
return generate_json_error_response(e)
|
||||
|
||||
cookie_dict = get_cookie_dict_from_headers(request)
|
||||
user_id = unquote(cookie_dict.get("user_id").value) if "user_id" in cookie_dict else "Guest"
|
||||
return frappe.session.user == user_id
|
||||
return True
|
||||
|
||||
def authenticate_client_id(self, client_id, request, *args, **kwargs):
|
||||
cli_id = frappe.db.get_value("OAuth Client", client_id, "name")
|
||||
|
|
@ -506,13 +494,6 @@ class OAuthWebRequestValidator(RequestValidator):
|
|||
return True
|
||||
|
||||
|
||||
def get_cookie_dict_from_headers(r):
|
||||
cookie = cookies.BaseCookie()
|
||||
if r.headers.get("Cookie"):
|
||||
cookie.load(r.headers.get("Cookie"))
|
||||
return cookie
|
||||
|
||||
|
||||
def calculate_at_hash(access_token, hash_alg):
|
||||
"""Helper method for calculating an access token
|
||||
hash, as described in http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@
|
|||
"description": "Letter Head in HTML",
|
||||
"fieldname": "content",
|
||||
"fieldtype": "HTML Editor",
|
||||
"ignore_xss_filter": 1,
|
||||
"label": "Header HTML",
|
||||
"oldfieldname": "content",
|
||||
"oldfieldtype": "Text Editor"
|
||||
|
|
@ -113,6 +114,7 @@
|
|||
"description": "Footer will display correctly only in PDF",
|
||||
"fieldname": "footer",
|
||||
"fieldtype": "HTML Editor",
|
||||
"ignore_xss_filter": 1,
|
||||
"label": "Footer HTML"
|
||||
},
|
||||
{
|
||||
|
|
@ -184,6 +186,7 @@
|
|||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval: doc.header_script || doc.footer_script",
|
||||
"depends_on": "eval: !doc.__islocal",
|
||||
"fieldname": "scripts_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Scripts"
|
||||
|
|
@ -200,7 +203,7 @@
|
|||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"max_attachments": 3,
|
||||
"modified": "2024-04-12 10:30:25.793932",
|
||||
"modified": "2026-02-24 20:53:14.297567",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Printing",
|
||||
"name": "Letter Head",
|
||||
|
|
@ -223,8 +226,9 @@
|
|||
"role": "Desk User"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -706,7 +706,10 @@ frappe.ui.form.PrintView = class {
|
|||
return;
|
||||
}
|
||||
} else {
|
||||
this.is_wkhtmltopdf_valid();
|
||||
let pdf_generator = this.get_pdf_generator(print_format?.pdf_generator);
|
||||
if (pdf_generator === "wkhtmltopdf") {
|
||||
this.is_wkhtmltopdf_valid();
|
||||
}
|
||||
this.render_page(
|
||||
"/api/method/frappe.utils.print_format.download_pdf?",
|
||||
false,
|
||||
|
|
@ -738,9 +741,7 @@ frappe.ui.form.PrintView = class {
|
|||
encodeURIComponent(this.get_letterhead()) +
|
||||
"&settings=" +
|
||||
encodeURIComponent(JSON.stringify(this.additional_settings)) +
|
||||
(this.lang_code ? "&_lang=" + this.lang_code : "") +
|
||||
"&pdf_generator=" +
|
||||
encodeURIComponent(pdf_generator)
|
||||
(this.lang_code ? "&_lang=" + this.lang_code : "")
|
||||
)
|
||||
);
|
||||
if (!w) {
|
||||
|
|
|
|||
|
|
@ -291,8 +291,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
set_primary_action() {
|
||||
if (this.can_create && !frappe.boot.read_only) {
|
||||
const doctype_name = __(frappe.router.doctype_layout) || __(this.doctype);
|
||||
const add_button_label = __("Add {0}", [doctype_name], "Primary action in list view");
|
||||
const create_button = this.page.set_primary_action(
|
||||
__("Add {0}", [doctype_name], "Primary action in list view"),
|
||||
add_button_label,
|
||||
() => {
|
||||
if (this.settings.primary_action) {
|
||||
this.settings.primary_action();
|
||||
|
|
@ -304,12 +305,26 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
);
|
||||
if (frappe.is_mobile()) {
|
||||
create_button.append(__("Add"));
|
||||
} else {
|
||||
this._trim_primary_action_if_overflow(create_button, add_button_label);
|
||||
}
|
||||
} else {
|
||||
this.page.clear_primary_action();
|
||||
}
|
||||
}
|
||||
|
||||
_trim_primary_action_if_overflow(btn, add_button_label) {
|
||||
const container = this.page.wrapper.find(".page-head-content")[0];
|
||||
if (!container || !btn[0]) return;
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const btnRect = btn[0].getBoundingClientRect();
|
||||
if (btnRect.right > containerRect.right) {
|
||||
const short_label = __("Add");
|
||||
btn.attr("title", add_button_label).tooltip();
|
||||
btn.find("span").text(short_label);
|
||||
}
|
||||
}
|
||||
|
||||
make_new_doc() {
|
||||
const doctype = this.doctype;
|
||||
const options = {};
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
<p>
|
||||
<a class="onboarding-sidebar">
|
||||
{%= frappe.utils.icon("user-check" , "sm", "", "", "text-ink-gray-7 current-color", true)%}
|
||||
<span> {%= __("Getting started") %} </span>
|
||||
<span> {%= __("Getting Started") %} </span>
|
||||
</a>
|
||||
</p>
|
||||
<a class="collapse-sidebar-link">
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ function updateSettings(step) {
|
|||
};
|
||||
|
||||
frappe.set_route("Form", step.reference_document);
|
||||
markComplete(step);
|
||||
}
|
||||
|
||||
async function createEntry(step) {
|
||||
|
|
@ -263,10 +264,10 @@ function markReset(step) {
|
|||
</div>
|
||||
|
||||
<div v-if="skippAll">
|
||||
<span class="onb-skip" @click="resetAll(steps)"> {{ __("Reset all") }}</span>
|
||||
<span class="onb-skip" @click="resetAll(steps)"> {{ __("Reset All") }}</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span class="onb-skip" @click="skipAll(steps)">Skip all</span>
|
||||
<span class="onb-skip" @click="skipAll(steps)">{{ __("Skip All") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -215,6 +215,46 @@ function addStyles() {
|
|||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .onb-panel {
|
||||
background-color: #232323;
|
||||
color: #e5e7eb;
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.6);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .text-base {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .onb-skip {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .onb-skip:hover {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .onb-title-steps,
|
||||
[data-theme="dark"] .onb-progress-text {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .onb-group:hover {
|
||||
background: #374151;
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .onb-progress-badge {
|
||||
background: rgba(245,158,11,0.15);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .onb-progress-badge-complete {
|
||||
background: rgba(16,185,129,0.15);
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
|
||||
`;
|
||||
|
||||
document.head.appendChild(style);
|
||||
|
|
|
|||
|
|
@ -1575,7 +1575,8 @@ Object.assign(frappe.utils, {
|
|||
if (item.is_query_report) {
|
||||
route = "query-report/" + item.name;
|
||||
} else if (!item.is_query_report && item.report_ref_doctype) {
|
||||
route = frappe.router.slug(item.report_ref_doctype) + "/view/report/";
|
||||
route =
|
||||
frappe.router.slug(item.report_ref_doctype) + "/view/report/" + item.name;
|
||||
} else {
|
||||
route = "report/" + item.name;
|
||||
}
|
||||
|
|
@ -1909,7 +1910,13 @@ Object.assign(frappe.utils, {
|
|||
|
||||
process_filter_expression(filter) {
|
||||
let filters = [];
|
||||
filters = filter ? new Function(`return ${filter}`)() : [];
|
||||
if (filter) {
|
||||
try {
|
||||
filters = JSON.parse(filter);
|
||||
} catch {
|
||||
console.warn("Invalid JSON in filter expression", filter);
|
||||
}
|
||||
}
|
||||
return this.cleanup_filters(filters);
|
||||
},
|
||||
cleanup_filters(filters) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import os
|
|||
from urllib.parse import urlparse
|
||||
|
||||
import frappe
|
||||
from frappe.website.page_renderers.document_page import _find_matching_document_webview
|
||||
from frappe.website.page_renderers.template_page import TemplatePage
|
||||
from frappe.website.utils import can_cache
|
||||
|
||||
|
|
@ -26,10 +27,26 @@ class NotFoundPage(TemplatePage):
|
|||
|
||||
def can_cache_404(self):
|
||||
# do not cache 404 for custom homepages
|
||||
return can_cache() and self.request_url and not self.is_custom_home_page()
|
||||
# also skip caching docs with website permission checks (access is dynamic)
|
||||
return (
|
||||
can_cache()
|
||||
and self.request_url
|
||||
and not self.is_custom_home_page()
|
||||
and not self.has_website_permission_check()
|
||||
)
|
||||
|
||||
def is_custom_home_page(self):
|
||||
url_parts = urlparse(self.request_url)
|
||||
request_url = os.path.splitext(url_parts.path)[0]
|
||||
request_path = os.path.splitext(self.request_path)[0]
|
||||
return request_url in HOMEPAGE_PATHS and request_path not in HOMEPAGE_PATHS
|
||||
|
||||
def has_website_permission_check(self):
|
||||
request_path = os.path.splitext(self.request_path)[0]
|
||||
if not (document := _find_matching_document_webview(request_path)):
|
||||
return False
|
||||
doctype, docname = document
|
||||
doc = frappe.get_cached_doc(doctype, docname)
|
||||
return hasattr(doc, "has_website_permission") or bool(
|
||||
frappe.get_hooks("has_website_permission", {}).get(doctype)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
{{ _("Print") }}
|
||||
</a>
|
||||
<a class="p-2"
|
||||
href="/api/method/frappe.utils.print_format.download_pdf?doctype={{doctype|e}}&name={{name|e}}&format={{print_format|e}}&letterhead={{letterhead|e}}&no_letterhead={{no_letterhead|e}}&_lang={{lang|e}}&key={{key|e}}">
|
||||
href="/api/method/frappe.utils.print_format.download_pdf?doctype={{doctype|e}}&name={{name|e}}&format={{print_format|e}}&letterhead={{letterhead|e}}&no_letterhead={{no_letterhead|e}}&_lang={{lang|e}}&key={{key|e}}&pdf_generator={{pdf_generator|e}}">
|
||||
{{ _('Get PDF') }}
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ def get_context(context) -> PrintContext:
|
|||
|
||||
# Include selected print format name in access log
|
||||
print_format_name = getattr(print_format, "name", "Standard")
|
||||
pdf_generator = getattr(print_format, "pdf_generator", "wkhtmltopdf")
|
||||
|
||||
make_access_log(
|
||||
doctype=frappe.form_dict.doctype,
|
||||
|
|
@ -114,7 +115,7 @@ def get_context(context) -> PrintContext:
|
|||
"print_format": print_format_name,
|
||||
"letterhead": letterhead,
|
||||
"no_letterhead": frappe.form_dict.no_letterhead,
|
||||
"pdf_generator": frappe.form_dict.get("pdf_generator", "wkhtmltopdf"),
|
||||
"pdf_generator": frappe.form_dict.get("pdf_generator", pdf_generator),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2055,9 +2055,9 @@ mime@^1.4.1:
|
|||
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
||||
|
||||
minimatch@^3.1.1:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.3.tgz#6a5cba9b31f503887018f579c89f81f61162e624"
|
||||
integrity sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue