Merge branch 'develop' into fix-note-2

This commit is contained in:
Raffael Meyer 2023-02-17 11:04:26 +01:00 committed by GitHub
commit 97dde45067
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 1437 additions and 1182 deletions

View file

@ -96,4 +96,4 @@ jobs:
pip install pip-audit
cd ${GITHUB_WORKSPACE}
sed -i '/dropbox/d' pyproject.toml # Remove dropbox temporarily https://github.com/dropbox/dropbox-sdk-python/pull/456
pip-audit .
pip-audit --desc on .

View file

@ -71,23 +71,3 @@ pull_request_rules:
assignees:
- "{{ author }}"
- name: backport to version-13-pre-release
conditions:
- label="backport version-13-pre-release"
actions:
backport:
branches:
- version-13-pre-release
assignees:
- "{{ author }}"
- name: backport to version-12-hotfix
conditions:
- label="backport version-12-hotfix"
actions:
backport:
branches:
- version-12-hotfix
assignees:
- "{{ author }}"

View file

@ -41,9 +41,6 @@ def application(request: Request):
init_request(request)
frappe.recorder.record()
frappe.monitor.start()
frappe.rate_limiter.apply()
frappe.api.validate_auth()
if request.method == "OPTIONS":
@ -74,15 +71,14 @@ def application(request: Request):
response = handle_exception(e)
else:
rollback = after_request(rollback)
rollback = sync_database(rollback)
finally:
if request.method in UNSAFE_HTTP_METHODS and frappe.db and rollback:
frappe.db.rollback()
frappe.rate_limiter.update()
frappe.monitor.stop(response)
frappe.recorder.dump()
for after_request_task in frappe.get_hooks("after_request"):
frappe.call(after_request_task, response=response, request=request)
log_request(request, response)
process_response(response)
@ -119,6 +115,9 @@ def init_request(request):
if request.method != "OPTIONS":
frappe.local.http_request = HTTPRequest()
for before_request_task in frappe.get_hooks("before_request"):
frappe.call(before_request_task)
def setup_read_only_mode():
"""During maintenance_mode reads to DB can still be performed to reduce downtime. This
@ -318,7 +317,7 @@ def handle_exception(e):
return response
def after_request(rollback):
def sync_database(rollback: bool) -> bool:
# if HTTP method would change server state, commit if necessary
if (
frappe.db
@ -332,9 +331,8 @@ def after_request(rollback):
rollback = False
# update session
if getattr(frappe.local, "session_obj", None):
updated_in_db = frappe.local.session_obj.update()
if updated_in_db:
if session := getattr(frappe.local, "session_obj", None):
if session.update():
frappe.db.commit()
rollback = False
@ -376,6 +374,7 @@ def serve(
"0.0.0.0",
int(port),
application,
exclude_patterns=["test_*"],
use_reloader=False if in_test_env else not no_reload,
use_debugger=not in_test_env,
use_evalex=not in_test_env,

View file

@ -307,7 +307,7 @@ class LoginManager:
current_hour = int(now_datetime().strftime("%H"))
if login_before and current_hour > login_before:
if login_before and current_hour >= login_before:
frappe.throw(_("Login not allowed at this time"), frappe.AuthenticationError)
if login_after and current_hour < login_after:

View file

@ -101,7 +101,7 @@ def get_bootinfo():
bootinfo.app_logo_url = get_app_logo()
bootinfo.link_title_doctypes = get_link_title_doctypes()
bootinfo.translated_doctypes = get_translated_doctypes()
bootinfo.subscription_expiry = add_subscription_expiry()
bootinfo.subscription_conf = add_subscription_conf()
return bootinfo
@ -234,7 +234,7 @@ def get_user_pages_or_reports(parent, cache=False):
has_role[p.name] = {"modified": p.modified, "title": p.title}
elif parent == "Report":
reports = frappe.get_all(
reports = frappe.get_list(
"Report",
fields=["name", "report_type"],
filters={"name": ("in", has_role.keys())},
@ -243,6 +243,10 @@ def get_user_pages_or_reports(parent, cache=False):
for report in reports:
has_role[report.name]["report_type"] = report.report_type
non_permitted_reports = set(has_role.keys()) - {r.name for r in reports}
for r in non_permitted_reports:
has_role.pop(r, None)
# Expire every six hours
_cache.set_value("has_role:" + parent, has_role, frappe.session.user, 21600)
return has_role
@ -431,8 +435,8 @@ def load_currency_docs(bootinfo):
bootinfo.docs += currency_docs
def add_subscription_expiry():
def add_subscription_conf():
try:
return frappe.conf.subscription["expiry"]
return frappe.conf.subscription
except Exception:
return ""

View file

@ -5,7 +5,6 @@ import click
import frappe
from frappe.commands import get_site, pass_context
from frappe.exceptions import SiteNotSpecifiedError
from frappe.utils import cint
@click.command("trigger-scheduler-event", help="Trigger a scheduler event")
@ -74,36 +73,40 @@ def disable_scheduler(context):
@click.command("scheduler")
@click.option("--site", help="site name")
@click.argument("state", type=click.Choice(["pause", "resume", "disable", "enable"]))
@click.argument("state", type=click.Choice(["pause", "resume", "disable", "enable", "status"]))
@click.option(
"--format", "-f", default="text", type=click.Choice(["json", "text"]), help="Output format"
)
@click.option("--verbose", "-v", is_flag=True, help="Verbose output")
@pass_context
def scheduler(context, state, site=None):
def scheduler(context, state: str, format: str, verbose: bool = False, site: str | None = None):
"""Control scheduler state."""
import frappe.utils.scheduler
from frappe.installer import update_site_config
import frappe
from frappe.utils.scheduler import is_scheduler_inactive, toggle_scheduler
if not site:
site = get_site(context)
site = site or get_site(context)
try:
frappe.init(site=site)
output = {
"text": "Scheduler is {status} for site {site}",
"json": '{{"status": "{status}", "site": "{site}"}}',
}
if state == "pause":
update_site_config("pause_scheduler", 1)
elif state == "resume":
update_site_config("pause_scheduler", 0)
elif state == "disable":
frappe.connect()
frappe.utils.scheduler.disable_scheduler()
frappe.db.commit()
elif state == "enable":
frappe.connect()
frappe.utils.scheduler.enable_scheduler()
frappe.db.commit()
with frappe.init_site(site=site):
match state:
case "status":
frappe.connect()
status = "disabled" if is_scheduler_inactive(verbose=verbose) else "enabled"
return print(output[format].format(status=status, site=site))
case "pause" | "resume":
from frappe.installer import update_site_config
print(f"Scheduler {state}d for site {site}")
update_site_config("pause_scheduler", state == "pause")
case "enable" | "disable":
frappe.connect()
toggle_scheduler(state == "enable")
frappe.db.commit()
finally:
frappe.destroy()
print(output[format].format(status=f"{state}d", site=site))
@click.command("set-maintenance-mode")

View file

@ -44,7 +44,7 @@ from frappe.exceptions import SiteNotSpecifiedError
@click.option(
"--force", help="Force restore if site/database already exists", is_flag=True, default=False
)
@click.option("--source_sql", help="Initiate database with a SQL file")
@click.option("--source-sql", "--source_sql", help="Initiate database with a SQL file")
@click.option("--install-app", multiple=True, help="Install app after installation")
@click.option(
"--set-default", is_flag=True, default=False, help="Set the new site as default site"
@ -67,10 +67,13 @@ def new_site(
set_default=False,
):
"Create a new site"
from frappe.installer import _new_site
from frappe.installer import _new_site, extract_sql_from_archive
frappe.init(site=site, new_site=True)
if source_sql:
source_sql = extract_sql_from_archive(source_sql)
_new_site(
db_name,
site,

View file

@ -908,7 +908,7 @@ def run_ui_tests(
os.chdir(app_base_path)
node_bin = subprocess.getoutput("npm bin")
node_bin = subprocess.getoutput("yarn bin")
cypress_path = f"{node_bin}/cypress"
drag_drop_plugin_path = f"{node_bin}/../@4tw/cypress-drag-drop"
real_events_plugin_path = f"{node_bin}/../cypress-real-events"

View file

@ -94,6 +94,7 @@
"fieldtype": "Select",
"hidden": 1,
"label": "Status",
"no_copy": 1,
"options": "Pending\nSuccess\nPartial Success\nError",
"read_only": 1
},
@ -170,7 +171,7 @@
],
"hide_toolbar": 1,
"links": [],
"modified": "2022-02-01 20:08:37.624914",
"modified": "2022-02-14 10:08:37.624914",
"modified_by": "Administrator",
"module": "Core",
"name": "Data Import",
@ -194,4 +195,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View file

@ -104,88 +104,7 @@ frappe.ui.form.on("DocType", {
frappe.ui.form.on("DocField", {
form_render(frm, doctype, docname) {
// Render two select fields for Fetch From instead of Small Text for better UX
let field = frm.cur_grid.grid_form.fields_dict.fetch_from;
$(field.input_area).hide();
let $doctype_select = $(`<select class="form-control">`);
let $field_select = $(`<select class="form-control">`);
let $wrapper = $('<div class="fetch-from-select row"><div>');
$wrapper.append($doctype_select, $field_select);
field.$input_wrapper.append($wrapper);
$doctype_select.wrap('<div class="col"></div>');
$field_select.wrap('<div class="col"></div>');
let row = frappe.get_doc(doctype, docname);
let curr_value = { doctype: null, fieldname: null };
if (row.fetch_from) {
let [doctype, fieldname] = row.fetch_from.split(".");
curr_value.doctype = doctype;
curr_value.fieldname = fieldname;
}
let doctypes = frm.doc.fields
.filter((df) => df.fieldtype == "Link")
.filter((df) => df.options && df.fieldname != row.fieldname)
.sort((a, b) => a.options.localeCompare(b.options))
.map((df) => ({
label: `${df.options} (${df.fieldname})`,
value: df.fieldname,
}));
$doctype_select.add_options([
{ label: __("Select DocType"), value: "", selected: true },
...doctypes,
]);
$doctype_select.on("change", () => {
row.fetch_from = "";
frm.dirty();
update_fieldname_options();
});
function update_fieldname_options() {
$field_select.find("option").remove();
let link_fieldname = $doctype_select.val();
if (!link_fieldname) return;
let link_field = frm.doc.fields.find((df) => df.fieldname === link_fieldname);
let link_doctype = link_field.options;
frappe.model.with_doctype(link_doctype, () => {
let fields = frappe.meta
.get_docfields(link_doctype, null, {
fieldtype: ["not in", frappe.model.no_value_type],
})
.sort((a, b) => a.label.localeCompare(b.label))
.map((df) => ({
label: `${df.label} (${df.fieldtype})`,
value: df.fieldname,
}));
$field_select.add_options([
{
label: __("Select Field"),
value: "",
selected: true,
disabled: true,
},
...fields,
]);
if (curr_value.fieldname) {
$field_select.val(curr_value.fieldname);
}
});
}
$field_select.on("change", () => {
let fetch_from = `${$doctype_select.val()}.${$field_select.val()}`;
row.fetch_from = fetch_from;
frm.dirty();
});
if (curr_value.doctype) {
$doctype_select.val(curr_value.doctype);
update_fieldname_options();
}
frm.trigger("setup_fetch_from_fields", doctype, docname);
},
fieldtype: function (frm) {

View file

@ -122,11 +122,20 @@ class User(Document):
now = frappe.flags.in_test or frappe.flags.in_install
self.send_password_notification(self.__new_password)
frappe.enqueue(
"frappe.core.doctype.user.user.create_contact", user=self, ignore_mandatory=True, now=now
"frappe.core.doctype.user.user.create_contact",
user=self,
ignore_mandatory=True,
now=now,
enqueue_after_commit=True,
)
if self.name not in STANDARD_USERS and not self.user_image:
frappe.enqueue("frappe.core.doctype.user.user.update_gravatar", name=self.name, now=now)
frappe.enqueue(
"frappe.core.doctype.user.user.update_gravatar",
name=self.name,
now=now,
enqueue_after_commit=True,
)
# Set user selected timezone
if self.time_zone:

View file

@ -263,6 +263,10 @@ frappe.ui.form.on("Customize Form Field", {
f.is_custom_field = true;
frm.trigger("setup_default_views");
},
form_render(frm, doctype, docname) {
frm.trigger("setup_fetch_from_fields", doctype, docname);
},
});
// can't delete standard links

View file

@ -43,7 +43,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-09-01 03:22:33.973058",
"modified": "2023-02-14 17:53:24.486171",
"modified_by": "Administrator",
"module": "Custom",
"name": "DocType Layout",
@ -64,7 +64,7 @@
},
{
"read": 1,
"role": "Guest"
"role": "All"
}
],
"route": "doctype-layout",

View file

@ -363,7 +363,7 @@ def get_events(start, end, user=None, for_reminder=False, filters=None) -> list[
# last day of month issue, start from prev month!
try:
getdate(date)
except ValueError:
except Exception:
date = date.split("-")
date = date[0] + "-" + str(cint(date[1]) - 1) + "-" + date[2]

View file

@ -207,7 +207,7 @@ class FormMeta(Meta):
if df.get("is_custom_field"):
custom_field_link = get_link_to_form("Custom Field", df.name)
msg += " " + _("Please delete the field from {2} or add the required doctype.").format(
msg += " " + _("Please delete the field from {0} or add the required doctype.").format(
custom_field_link
)

View file

@ -1,16 +1,6 @@
// Copyright (c) 2018, Frappe Technologies and contributors
// For license information, please see license.txt
this.frm.add_fetch("sender", "email_id", "sender_email");
this.frm.fields_dict.sender.get_query = function () {
return {
filters: {
enable_outgoing: 1,
},
};
};
frappe.notification = {
setup_fieldname_select: function (frm) {
// get the doctype to update fields
@ -156,6 +146,15 @@ frappe.ui.form.on("Notification", {
refresh: function (frm) {
frappe.notification.setup_fieldname_select(frm);
frappe.notification.setup_example_message(frm);
frm.add_fetch("sender", "email_id", "sender_email");
frm.set_query("sender", () => {
return {
filters: {
enable_outgoing: 1,
},
};
});
frm.get_field("is_standard").toggle(frappe.boot.developer_mode);
frm.trigger("event");
},

View file

@ -218,7 +218,6 @@ scheduler_events = {
"frappe.automation.doctype.auto_repeat.auto_repeat.set_auto_repeat_as_completed",
"frappe.email.doctype.unhandled_email.unhandled_email.remove_old_unhandled_emails",
"frappe.core.doctype.log_settings.log_settings.run_log_clean_up",
"frappe.utils.subscription.enable_manage_subscription",
],
"daily_long": [
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily",
@ -392,3 +391,20 @@ ignore_links_on_delete = [
"Integration Request",
"Unhandled Email",
]
# Request Hooks
before_request = [
"frappe.recorder.record",
"frappe.monitor.start",
"frappe.rate_limiter.apply",
]
after_request = ["frappe.rate_limiter.update", "frappe.monitor.stop", "frappe.recorder.dump"]
# Background Job Hooks
before_job = [
"frappe.monitor.start",
]
after_job = [
"frappe.monitor.stop",
"frappe.utils.file_lock.release_document_locks",
]

View file

@ -95,6 +95,7 @@ optional_fields = ("_user_tags", "_comments", "_assign", "_liked_by", "_seen")
table_fields = ("Table", "Table MultiSelect")
core_doctypes_list = (
"DefaultValue",
"DocType",
"DocField",
"DocPerm",
@ -199,14 +200,13 @@ def get_permitted_fields(
if doctype in core_doctypes_list:
return valid_columns
meta_fields = meta.default_fields.copy()
optional_meta_fields = [x for x in optional_fields if x in valid_columns]
if permitted_fields := meta.get_permitted_fieldnames(parenttype=parenttype, user=user):
meta_fields = meta.default_fields.copy()
optional_meta_fields = [x for x in optional_fields if x in valid_columns]
if meta.istable:
meta_fields.extend(child_table_fields)
if meta.istable:
meta_fields.extend(child_table_fields)
return (
meta_fields
+ meta.get_permitted_fieldnames(parenttype=parenttype, user=user)
+ optional_meta_fields
)
return meta_fields + permitted_fields + optional_meta_fields
return []

View file

@ -762,7 +762,7 @@ class BaseDocument:
values.name = doctype
if frappe.get_meta(doctype).get("is_virtual"):
values = frappe.get_doc(doctype, docname)
values = frappe.get_doc(doctype, docname).as_dict()
if values:
setattr(self, df.fieldname, values.name)

View file

@ -164,6 +164,7 @@ class DatabaseQuery:
self.run = run
self.strict = strict
self.ignore_ddl = ignore_ddl
self.parent_doctype = parent_doctype
# for contextual user permission check
# to determine which user permission is applicable on link field of specific doctype
@ -588,19 +589,34 @@ class DatabaseQuery:
self.fields.pop(idx)
def apply_fieldlevel_read_permissions(self):
"""Apply fieldlevel read permissions to the query"""
"""Apply fieldlevel read permissions to the query
Note: Does not apply to `frappe.model.core_doctype_list`
Remove fields that user is not allowed to read. If `fields=["*"]` is passed, only permitted fields will
be returned.
Example:
- User has read permission only on `title` for DocType `Note`
- Query: fields=["*"]
- Result: fields=["title", ...] // will also include Frappe's meta field like `name`, `owner`, etc.
"""
if self.flags.ignore_permissions:
return
asterisk_fields = []
permitted_fields = get_permitted_fields(doctype=self.doctype)
permitted_fields = get_permitted_fields(doctype=self.doctype, parenttype=self.parent_doctype)
for i, field in enumerate(self.fields):
if "distinct" in field.lower():
# field: 'count(distinct `tabPhoto`.name) as total_count'
# column: 'tabPhoto.name'
self.distinct = True
column = field.split(" ", 2)[1].replace("`", "").replace(")", "")
if _fn := FN_PARAMS_PATTERN.findall(field):
column = _fn[0].replace("distinct ", "").replace("DISTINCT ", "").replace("`", "")
# field: 'distinct name'
# column: 'name'
else:
column = field.split(" ", 2)[1].replace("`", "")
else:
# field: 'count(`tabPhoto`.name) as total_count'
# column: 'tabPhoto.name'
@ -628,7 +644,7 @@ class DatabaseQuery:
permitted_child_table_fields = get_permitted_fields(
doctype=ch_doctype, parenttype=self.doctype
)
if column in permitted_child_table_fields:
if column in permitted_child_table_fields or column in optional_fields:
continue
else:
self.remove_field(i)

View file

@ -533,16 +533,25 @@ class Meta(Document):
return self.high_permlevel_fields
def get_permitted_fieldnames(self, parenttype=None, *, user=None):
"""Build list of `fieldname` with read perm level and all the higher perm levels defined."""
if not hasattr(self, "permitted_fieldnames"):
self.permitted_fieldnames = []
permlevel_access = set(self.get_permlevel_access("read", parenttype, user=user))
"""Build list of `fieldname` with read perm level and all the higher perm levels defined.
for df in self.get_fieldnames_with_value(with_field_meta=True, with_virtual_fields=True):
if df.permlevel in permlevel_access:
self.permitted_fieldnames.append(df.fieldname)
Note: If permissions are not defined for DocType, return all the fields with value.
"""
permitted_fieldnames = []
return self.permitted_fieldnames
if self.istable and not parenttype:
return permitted_fieldnames
if not self.get_permissions(parenttype=parenttype):
return self.get_fieldnames_with_value()
permlevel_access = set(self.get_permlevel_access("read", parenttype, user=user))
for df in self.get_fieldnames_with_value(with_field_meta=True, with_virtual_fields=True):
if df.permlevel in permlevel_access:
permitted_fieldnames.append(df.fieldname)
return permitted_fieldnames
def get_permlevel_access(self, permission_type="read", parenttype=None, *, user=None):
has_access_to = []
@ -772,7 +781,7 @@ def trim_tables(doctype=None, dry_run=False, quiet=False):
delete the db field.
"""
UPDATED_TABLES = {}
filters = {"issingle": 0}
filters = {"issingle": 0, "is_virtual": 0}
if doctype:
filters["name"] = doctype

View file

@ -215,7 +215,6 @@ frappe.patches.v14_0.update_multistep_webforms
execute:frappe.delete_doc('Page', 'background_jobs', ignore_missing=True, force=True)
frappe.patches.v14_0.drop_unused_indexes
frappe.patches.v15_0.drop_modified_index
frappe.patches.v14_0.add_manage_subscriptions_in_navbar_settings
frappe.patches.v14_0.update_attachment_comment
frappe.patches.v15_0.set_contact_full_name
execute:frappe.delete_doc("Page", "activity", force=1)

View file

@ -12,7 +12,6 @@ def execute():
for theme in themes:
doc = frappe.get_doc("Website Theme", theme.name)
try:
doc.generate_bootstrap_theme()
doc.save()
except Exception:
print("Ignoring....")

View file

@ -8,21 +8,31 @@ def execute():
for theme in frappe.get_all("Website Theme"):
doc = frappe.get_doc("Website Theme", theme.name)
setup_color_record(doc)
if not doc.get("custom_scss") and doc.theme_scss:
# move old theme to new theme
doc.custom_scss = doc.theme_scss
if doc.background_color:
setup_color_record(doc.background_color)
doc.save()
def setup_color_record(color):
frappe.get_doc(
{
"doctype": "Color",
"__newname": color,
"color": color,
}
).save()
def setup_color_record(doc):
color_fields = [
"primary_color",
"text_color",
"light_color",
"dark_color",
"background_color",
]
for color_field in color_fields:
color_code = doc.get(color_field)
if not color_code or frappe.db.exists("Color", color_code):
continue
frappe.get_doc(
{
"doctype": "Color",
"__newname": color_code,
"color": color_code,
}
).insert()

View file

@ -1,25 +0,0 @@
import frappe
def execute():
navbar_settings = frappe.get_single("Navbar Settings")
if frappe.db.exists("Navbar Item", {"item_label": "Manage Subscriptions"}):
return
for idx, row in enumerate(navbar_settings.settings_dropdown[2:], start=4):
row.idx = idx
navbar_settings.append(
"settings_dropdown",
{
"item_label": "Manage Subscriptions",
"item_type": "Action",
"action": "frappe.ui.toolbar.redirectToUrl()",
"is_standard": 1,
"hidden": 1,
"idx": 3,
},
)
navbar_settings.save()

View file

@ -41,7 +41,7 @@ frappe.ui.form.on("Print Format", {
}
if (frappe.model.can_write("Customize Form")) {
frappe.model.with_doctype(frm.doc.doc_type, function () {
let current_format = frappe.get_meta(frm.doc.DocType).default_print_format;
let current_format = frappe.get_meta(frm.doc.doc_type).default_print_format;
if (current_format == frm.doc.name) {
return;
}

View file

@ -17,10 +17,10 @@
</div>
<div class="checkbox">
<label>
<input type="checkbox"
data-fieldname="{{ f.fieldname }}"
{{ selected ? "checked" : "" }}>
{{ __(f.label) }}
<span class="input-area">
<input type="checkbox" data-fieldname="{{ f.fieldname }}" {{ selected ? "checked" : "" }}>
</span>
<span class="label-area">{{ __(f.label) }}</span>
</label>
</div>
</div>

View file

@ -83,7 +83,6 @@ import "./frappe/ui/toolbar/search_utils.js";
import "./frappe/ui/toolbar/about.js";
import "./frappe/ui/toolbar/navbar.html";
import "./frappe/ui/toolbar/toolbar.js";
import "./frappe/ui/toolbar/subscription.js";
// import "./frappe/ui/toolbar/notifications.js";
import "./frappe/views/communication.js";
import "./frappe/views/translation_manager.js";

View file

@ -430,62 +430,12 @@ frappe.Application = class Application {
});
}
handle_session_expired() {
if (!frappe.app.session_expired_dialog) {
var dialog = new frappe.ui.Dialog({
title: __("Session Expired"),
keep_open: true,
fields: [
{
fieldtype: "Password",
fieldname: "password",
label: __("Please Enter Your Password to Continue"),
},
],
onhide: () => {
if (!dialog.logged_in) {
frappe.app.redirect_to_login();
}
},
});
dialog.get_field("password").disable_password_checks();
dialog.set_primary_action(__("Login"), () => {
dialog.set_message(__("Authenticating..."));
frappe.call({
method: "login",
args: {
usr: frappe.session.user,
pwd: dialog.get_values().password,
},
callback: (r) => {
if (r.message === "Logged In") {
dialog.logged_in = true;
// revert backdrop
$(".modal-backdrop").css({
opacity: "",
"background-color": "#334143",
});
}
dialog.hide();
},
statusCode: () => {
dialog.hide();
},
});
});
frappe.app.session_expired_dialog = dialog;
}
if (!frappe.app.session_expired_dialog.display) {
frappe.app.session_expired_dialog.show();
// add backdrop
$(".modal-backdrop").css({
opacity: 1,
"background-color": "#4B4C9D",
});
}
frappe.app.redirect_to_login();
}
redirect_to_login() {
window.location.href = "/";
window.location.href = `/login?redirect-to=${encodeURIComponent(
window.location.pathname + window.location.search
)}`;
}
set_favicon() {
var link = $('link[type="image/x-icon"]').remove().attr("href");

View file

@ -114,4 +114,90 @@ frappe.model.DocTypeController = class DocTypeController extends frappe.ui.form.
this.frm.set_df_property("fields", "reqd", this.frm.doc.autoname !== "Prompt");
}
setup_fetch_from_fields(doc, doctype, docname) {
let frm = this.frm;
// Render two select fields for Fetch From instead of Small Text for better UX
let field = frm.cur_grid.grid_form.fields_dict.fetch_from;
$(field.input_area).hide();
let $doctype_select = $(`<select class="form-control">`);
let $field_select = $(`<select class="form-control">`);
let $wrapper = $('<div class="fetch-from-select row"><div>');
$wrapper.append($doctype_select, $field_select);
field.$input_wrapper.append($wrapper);
$doctype_select.wrap('<div class="col"></div>');
$field_select.wrap('<div class="col"></div>');
let row = frappe.get_doc(doctype, docname);
let curr_value = { doctype: null, fieldname: null };
if (row.fetch_from) {
let [doctype, fieldname] = row.fetch_from.split(".");
curr_value.doctype = doctype;
curr_value.fieldname = fieldname;
}
let doctypes = frm.doc.fields
.filter((df) => df.fieldtype == "Link")
.filter((df) => df.options && df.fieldname != row.fieldname)
.sort((a, b) => a.options.localeCompare(b.options))
.map((df) => ({
label: `${df.options} (${df.fieldname})`,
value: df.fieldname,
}));
$doctype_select.add_options([
{ label: __("Select DocType"), value: "", selected: true },
...doctypes,
]);
$doctype_select.on("change", () => {
row.fetch_from = "";
frm.dirty();
update_fieldname_options();
});
function update_fieldname_options() {
$field_select.find("option").remove();
let link_fieldname = $doctype_select.val();
if (!link_fieldname) return;
let link_field = frm.doc.fields.find((df) => df.fieldname === link_fieldname);
let link_doctype = link_field.options;
frappe.model.with_doctype(link_doctype, () => {
let fields = frappe.meta
.get_docfields(link_doctype, null, {
fieldtype: ["not in", frappe.model.no_value_type],
})
.sort((a, b) => a.label.localeCompare(b.label))
.map((df) => ({
label: `${df.label} (${df.fieldtype})`,
value: df.fieldname,
}));
$field_select.add_options([
{
label: __("Select Field"),
value: "",
selected: true,
disabled: true,
},
...fields,
]);
if (curr_value.fieldname) {
$field_select.val(curr_value.fieldname);
}
});
}
$field_select.on("change", () => {
let fetch_from = `${$doctype_select.val()}.${$field_select.val()}`;
row.fetch_from = fetch_from;
frm.dirty();
});
if (curr_value.doctype) {
$doctype_select.val(curr_value.doctype);
update_fieldname_options();
}
}
};

View file

@ -17,7 +17,7 @@ frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control
<div class="form-group">
<div class="clearfix">
<label class="control-label" style="padding-right: 0px;"></label>
<span class="ml-1 help"></span>
<span class="help"></span>
</div>
<div class="control-input-wrapper">
<div class="control-input"></div>

View file

@ -63,7 +63,7 @@ export default class Grid {
let template = `
<div class="grid-field">
<label class="control-label">${__(this.df.label || "")}</label>
<span class="ml-1 help"></span>
<span class="help"></span>
<p class="text-muted small grid-description"></p>
<div class="grid-custom-buttons"></div>
<div class="form-grid-container">

View file

@ -176,12 +176,12 @@ frappe.ui.form.ScriptManager = class ScriptManager {
}
if (client_script) {
eval(client_script);
new Function(client_script)();
}
if (!this.frm.doctype_layout && doctype.__custom_js) {
try {
eval(doctype.__custom_js);
new Function(doctype.__custom_js)();
} catch (e) {
frappe.msgprint({
title: __("Error in Client Script"),

View file

@ -73,6 +73,8 @@ frappe.ui.form.setup_user_image_event = function (frm) {
field.make_input();
}
field.$input.trigger("attach_doc_image");
// close sidebar
frm.page.close_sidebar();
} else {
/// on remove event for a sidebar image wrapper remove attach file.
frm.attachments.remove_attachment_by_filename(

View file

@ -1832,7 +1832,6 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
this.disable_list_update = true;
bulk_operations.edit(this.get_checked_items(true), field_mappings, () => {
this.disable_list_update = false;
this.clear_checked_items();
this.refresh();
});
},

View file

@ -274,21 +274,18 @@ $.extend(frappe.model, {
init_doctype: function (doctype) {
var meta = locals.DocType[doctype];
if (meta.__list_js) {
eval(meta.__list_js);
}
if (meta.__custom_list_js) {
eval(meta.__custom_list_js);
}
if (meta.__calendar_js) {
eval(meta.__calendar_js);
}
if (meta.__map_js) {
eval(meta.__map_js);
}
if (meta.__tree_js) {
eval(meta.__tree_js);
for (const asset_key of [
"__list_js",
"__custom_list_js",
"__calendar_js",
"__map_js",
"__tree_js",
]) {
if (meta[asset_key]) {
new Function(meta[asset_key])();
}
}
if (meta.__templates) {
$.extend(frappe.templates, meta.__templates);
}

View file

@ -188,14 +188,15 @@ frappe.ui.Page = class Page {
}
setup_overlay_sidebar() {
this.sidebar.find(".close-sidebar").remove();
let overlay_sidebar = this.sidebar.find(".overlay-sidebar").addClass("opened");
$('<div class="close-sidebar">').hide().appendTo(this.sidebar).fadeIn();
let scroll_container = $("html").css("overflow-y", "hidden");
this.sidebar.find(".close-sidebar").on("click", (e) => close_sidebar(e));
this.sidebar.on("click", "button:not(.dropdown-toggle)", (e) => close_sidebar(e));
this.sidebar.find(".close-sidebar").on("click", (e) => this.close_sidebar(e));
this.sidebar.on("click", "button:not(.dropdown-toggle)", (e) => this.close_sidebar(e));
let close_sidebar = () => {
this.close_sidebar = () => {
scroll_container.css("overflow-y", "");
this.sidebar.find("div.close-sidebar").fadeOut(() => {
overlay_sidebar

View file

@ -1,80 +0,0 @@
$(document).on("startup", async () => {
if (!frappe.boot.setup_complete || !frappe.user.has_role("System Manager")) {
return;
}
const expiry = frappe.boot.subscription_expiry;
if (expiry) {
let diff_days =
frappe.datetime.get_day_diff(cstr(expiry), frappe.datetime.get_today()) - 1;
let subscription_string = __(
`Your subscription will end in ${cstr(diff_days).bold()} ${
diff_days > 1 ? "days" : "day"
}. After that your site will be suspended.`
);
let $bar = $(`
<div
class="position-fixed top-100 start-20 translate-middle shadow sm:rounded-lg py-2"
style="left: 10%; bottom:20px; width:80%; margin: auto; text-align: center; border-radius: 10px; background-color: rgb(240 249 255); z-index: 1"
>
<div
style="display: flex; align-items: center; justify-content: space-between; text-align: center;"
class="text-muted"
>
<p style="float: left; margin: auto; font-size: 17px">${subscription_string}</p>
<div style="display: flex; align-items: center; justify-content: space-between">
<button
type="button"
class="button-renew px-4 py-2 border border-transparent text-white hover:bg-indigo-700 focus:outline-none focus:ring-offset-2 focus:ring-indigo-500"
style="background-color: #0089FF; border-radius: 5px; margin-right: 10px; height: fit-content;"
>
Subscribe
</button>
<a
type="button"
class="dismiss-upgrade text-muted" data-dismiss="modal" aria-hidden="true" style="font-size:30px; margin-bottom: 5px; margin-right: 10px"
>
\u00d7
</a>
</div>
</div>
</div>
`);
$("footer").append($bar);
$bar.find(".dismiss-upgrade").on("click", () => {
$bar.remove();
});
$bar.find(".button-renew").on("click", () => {
redirectToUrl();
});
}
});
function redirectToUrl() {
frappe.call({
method: "frappe.utils.subscription.remote_login",
callback: (url) => {
if (url.message !== false) {
window.open(url.message, "_blank");
} else {
frappe.msgprint({
title: __("Message"),
indicator: "orange",
message: __("No active subscriptions found."),
});
}
},
});
}
$.extend(frappe.ui.toolbar, {
redirectToUrl() {
redirectToUrl();
},
});

View file

@ -135,6 +135,9 @@ select.form-control {
content: ' *';
color: var(--red-400);
}
.help:empty {
display: none;
}
.ql-editor:not(.read-mode) {
background-color: var(--control-bg);
}

View file

@ -36,6 +36,10 @@
top: 5px;
right: 10px;
}
.help {
display: none;
}
}
.dt-header {
@ -67,6 +71,10 @@
.checkbox {
margin: 7px 0 7px 8px;
.label-area {
display: none;
}
}
[data-fieldtype="Color"] .control-input {

View file

@ -2,6 +2,7 @@ import sys
from contextlib import contextmanager
from random import choice
from threading import Thread
from time import time
from unittest.mock import patch
import requests
@ -306,3 +307,36 @@ class TestReadOnlyMode(FrappeAPITestCase):
response = self.post(self.REQ_PATH, {"description": frappe.mock("paragraph"), "sid": self.sid})
self.assertEqual(response.status_code, 503)
self.assertEqual(response.json["exc_type"], "InReadOnlyMode")
class TestWSGIApp(FrappeAPITestCase):
def test_request_hooks(self):
self.addCleanup(lambda: _test_REQ_HOOK.clear())
get_hooks = frappe.get_hooks
def patch_request_hooks(event: str, *args, **kwargs):
patched_hooks = {
"before_request": ["frappe.tests.test_api.before_request"],
"after_request": ["frappe.tests.test_api.after_request"],
}
if event not in patched_hooks:
return get_hooks(event, *args, **kwargs)
return patched_hooks[event]
with patch("frappe.get_hooks", patch_request_hooks):
self.assertIsNone(_test_REQ_HOOK.get("before_request"))
self.assertIsNone(_test_REQ_HOOK.get("after_request"))
res = self.get("/api/method/ping")
self.assertEqual(res.json, {"message": "pong"})
self.assertLess(_test_REQ_HOOK.get("before_request"), _test_REQ_HOOK.get("after_request"))
_test_REQ_HOOK = {}
def before_request(*args, **kwargs):
_test_REQ_HOOK["before_request"] = time()
def after_request(*args, **kwargs):
_test_REQ_HOOK["after_request"] = time()

View file

@ -1,11 +1,19 @@
import time
from contextlib import contextmanager
from unittest.mock import patch
from rq import Queue
import frappe
from frappe.core.doctype.rq_job.rq_job import remove_failed_jobs
from frappe.tests.utils import FrappeTestCase
from frappe.utils.background_jobs import generate_qname, get_redis_conn
from frappe.utils.background_jobs import (
RQ_JOB_FAILURE_TTL,
RQ_RESULTS_TTL,
execute_job,
generate_qname,
get_redis_conn,
)
class TestBackgroundJobs(FrappeTestCase):
@ -44,6 +52,79 @@ class TestBackgroundJobs(FrappeTestCase):
# lesser is earlier
self.assertTrue(high_priority_job.get_position() < low_priority_job.get_position())
def test_enqueue_call(self):
with patch.object(Queue, "enqueue_call") as mock_enqueue_call:
frappe.enqueue(
"frappe.handler.ping",
queue="short",
timeout=300,
kwargs={"site": frappe.local.site},
)
mock_enqueue_call.assert_called_once_with(
execute_job,
on_success=None,
on_failure=None,
timeout=300,
kwargs={
"site": frappe.local.site,
"user": "Administrator",
"method": "frappe.handler.ping",
"event": None,
"job_name": "frappe.handler.ping",
"is_async": True,
"kwargs": {"kwargs": {"site": frappe.local.site}},
},
at_front=False,
failure_ttl=RQ_JOB_FAILURE_TTL,
result_ttl=RQ_RESULTS_TTL,
)
def test_job_hooks(self):
self.addCleanup(lambda: _test_JOB_HOOK.clear())
with freeze_local() as locals, frappe.init_site(locals.site), patch(
"frappe.get_hooks", patch_job_hooks
):
frappe.connect()
self.assertIsNone(_test_JOB_HOOK.get("before_job"))
r = execute_job(
site=frappe.local.site,
user="Administrator",
method="frappe.handler.ping",
event=None,
job_name="frappe.handler.ping",
is_async=True,
kwargs={},
)
self.assertEqual(r, "pong")
self.assertLess(_test_JOB_HOOK.get("before_job"), _test_JOB_HOOK.get("after_job"))
def fail_function():
return 1 / 0
_test_JOB_HOOK = {}
def before_job(*args, **kwargs):
_test_JOB_HOOK["before_job"] = time.time()
def after_job(*args, **kwargs):
_test_JOB_HOOK["after_job"] = time.time()
@contextmanager
def freeze_local():
locals = frappe.local
frappe.local = frappe.Local()
yield locals
frappe.local = locals
def patch_job_hooks(event: str):
return {
"before_job": ["frappe.tests.test_background_jobs.before_job"],
"after_job": ["frappe.tests.test_background_jobs.after_job"],
}[event]

View file

@ -1,5 +1,5 @@
import frappe
from frappe.boot import get_unseen_notes
from frappe.boot import get_unseen_notes, get_user_pages_or_reports
from frappe.desk.doctype.note.note import mark_as_seen
from frappe.tests.utils import FrappeTestCase
@ -26,3 +26,47 @@ class TestBootData(FrappeTestCase):
mark_as_seen(note.name)
unseen_notes = [d.title for d in get_unseen_notes()]
self.assertListEqual(unseen_notes, [])
def test_get_user_pages_or_reports_with_permission_query(self):
# Create a ToDo custom report with admin user
frappe.set_user("Administrator")
frappe.get_doc(
{
"doctype": "Report",
"ref_doctype": "ToDo",
"report_name": "Test Admin Report",
"report_type": "Report Builder",
"is_standard": "No",
}
).insert()
# Add permission query such that each user can only see their own custom reports
frappe.get_doc(
dict(
doctype="Server Script",
name="test_report_permission_query",
script_type="Permission Query",
reference_doctype="Report",
script="""conditions = f"(`tabReport`.is_standard = 'Yes' or `tabReport`.owner = '{frappe.session.user}')"
""",
)
).insert()
# Create a ToDo custom report with test user
frappe.set_user("test@example.com")
frappe.get_doc(
{
"doctype": "Report",
"ref_doctype": "ToDo",
"report_name": "Test User Report",
"report_type": "Report Builder",
"is_standard": "No",
}
).insert(ignore_permissions=True)
get_user_pages_or_reports("Report")
allowed_reports = frappe.cache().get_value("has_role:Report", user=frappe.session.user)
# Test user must not see admin user's report
self.assertNotIn("Test Admin Report", allowed_reports)
self.assertIn("Test User Report", allowed_reports)

View file

@ -33,6 +33,7 @@ from frappe.tests.utils import FrappeTestCase, timeout
from frappe.utils import add_to_date, get_bench_path, get_bench_relative_path, now
from frappe.utils.backups import BackupGenerator, fetch_latest_backups
from frappe.utils.jinja_globals import bundled_asset
from frappe.utils.scheduler import enable_scheduler, is_scheduler_inactive
_result: Result | None = None
TEST_SITE = "commands-site-O4PN2QKA.test" # added random string tag to avoid collisions
@ -773,3 +774,52 @@ class TestDBCli(BaseTestCommands):
def test_db_cli(self):
self.execute("bench --site {site} db-console", kwargs={"cmd_input": rb"\q"})
self.assertEqual(self.returncode, 0)
@run_only_if(db_type_is.MARIADB)
def test_db_cli_with_sql(self):
self.execute("bench --site {site} db-console -e 'select 1'")
self.assertEqual(self.returncode, 0)
self.assertIn("1", self.stdout)
class TestSchedulerCLI(BaseTestCommands):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.is_scheduler_active = not is_scheduler_inactive()
@classmethod
def tearDownClass(cls):
super().tearDownClass()
if cls.is_scheduler_active:
enable_scheduler()
def test_scheduler_status(self):
self.execute("bench --site {site} scheduler status")
self.assertEqual(self.returncode, 0)
self.assertRegex(self.stdout, r"Scheduler is (disabled|enabled) for site .*")
self.execute("bench --site {site} scheduler status -f json")
parsed_output = frappe.parse_json(self.stdout)
self.assertEqual(self.returncode, 0)
self.assertIsInstance(parsed_output, dict)
self.assertIn("status", parsed_output)
self.assertIn("site", parsed_output)
def test_scheduler_enable_disable(self):
self.execute("bench --site {site} scheduler disable")
self.assertEqual(self.returncode, 0)
self.assertRegex(self.stdout, r"Scheduler is disabled for site .*")
self.execute("bench --site {site} scheduler enable")
self.assertEqual(self.returncode, 0)
self.assertRegex(self.stdout, r"Scheduler is enabled for site .*")
def test_scheduler_pause_resume(self):
self.execute("bench --site {site} scheduler pause")
self.assertEqual(self.returncode, 0)
self.assertRegex(self.stdout, r"Scheduler is paused for site .*")
self.execute("bench --site {site} scheduler resume")
self.assertEqual(self.returncode, 0)
self.assertRegex(self.stdout, r"Scheduler is resumed for site .*")

View file

@ -986,6 +986,46 @@ class TestDBQuery(FrappeTestCase):
class TestReportView(FrappeTestCase):
def test_get_count(self):
frappe.local.request = frappe._dict()
frappe.local.request.method = "GET"
# test with data check field
frappe.local.form_dict = frappe._dict(
{
"doctype": "DocType",
"filters": [["DocType", "show_title_field_in_link", "=", 1]],
"fields": [],
"distinct": "false",
}
)
list_filter_response = execute_cmd("frappe.desk.reportview.get_count")
frappe.local.form_dict = frappe._dict(
{"doctype": "DocType", "filters": {"show_title_field_in_link": 1}, "distinct": "true"}
)
dict_filter_response = execute_cmd("frappe.desk.reportview.get_count")
self.assertIsInstance(list_filter_response, int)
self.assertEqual(list_filter_response, dict_filter_response)
# test with child table filter
frappe.local.form_dict = frappe._dict(
{
"doctype": "DocType",
"filters": [["DocField", "fieldtype", "=", "Data"]],
"fields": [],
"distinct": "true",
}
)
child_filter_response = execute_cmd("frappe.desk.reportview.get_count")
current_value = frappe.db.sql(
# the below query is equivalent to the one in reportview.get_count
"select distinct count(distinct `tabDocType`.name) as total_count"
" from `tabDocType` left join `tabDocField`"
" on (`tabDocField`.parenttype = 'DocType' and `tabDocField`.parent = `tabDocType`.name)"
" where `tabDocField`.`fieldtype` = 'Data'"
)[0][0]
self.assertEqual(child_filter_response, current_value)
def test_reportview_get(self):
user = frappe.get_doc("User", "test@example.com")
add_child_table_to_blog_post()

View file

@ -1,4 +1,8 @@
from contextlib import contextmanager
from random import choice
import frappe
from frappe.model import core_doctypes_list, get_permitted_fields
from frappe.model.utils import get_fetch_values
from frappe.tests.utils import FrappeTestCase
@ -25,3 +29,47 @@ class TestModelUtils(FrappeTestCase):
self.assertEqual(
get_fetch_values(doctype, "assigned_by", user), {"assigned_by_full_name": full_name}
)
def test_get_permitted_fields(self):
# Administrator should have access to all fields in ToDo
todo_all_fields = get_permitted_fields("ToDo", user="Administrator")
todo_all_columns = frappe.get_meta("ToDo").get_valid_columns()
self.assertListEqual(todo_all_fields, todo_all_columns)
# Guest should have access to no fields in ToDo
with set_user("Guest"):
guest_permitted_fields = get_permitted_fields("ToDo")
self.assertEqual(guest_permitted_fields, [])
# everyone should have access to all fields of core doctypes
with set_user("Guest"):
picked_doctype = choice(core_doctypes_list)
core_permitted_fields = get_permitted_fields(picked_doctype)
picked_doctype_all_columns = frappe.get_meta(picked_doctype).get_valid_columns()
self.assertSequenceEqual(core_permitted_fields, picked_doctype_all_columns)
# access to child tables' fields is restricted to no fields unless parent is passed & permitted
with set_user("Administrator"):
without_parent_fields = get_permitted_fields("Installed Application")
with_parent_fields = get_permitted_fields(
"Installed Application", parenttype="Installed Applications"
)
child_all_fields = frappe.get_meta("Installed Application").get_valid_columns()
self.assertEqual(without_parent_fields, [])
self.assertLess(len(without_parent_fields), len(with_parent_fields))
self.assertSequenceEqual(set(with_parent_fields), set(child_all_fields))
# guest has access to no fields
with set_user("Guest"):
self.assertEqual(get_permitted_fields("Installed Application"), [])
self.assertEqual(
get_permitted_fields("Installed Application", parenttype="Installed Applications"), []
)
@contextmanager
def set_user(user: str):
past_user = frappe.session.user or "Administrator"
frappe.set_user(user)
yield
frappe.set_user(past_user)

View file

@ -15,10 +15,7 @@ class TestSearch(FrappeTestCase):
def setUp(self):
if self._testMethodName == "test_link_field_order":
setup_test_link_field_order(self)
def tearDown(self):
if self._testMethodName == "test_link_field_order":
teardown_test_link_field_order(self)
self.addCleanup(teardown_test_link_field_order, self)
def test_search_field_sanitizer(self):
# pass
@ -146,24 +143,28 @@ def setup_test_link_field_order(TestCase):
TestCase.parent_doctype_name = "All Territories"
# Create Tree doctype
TestCase.tree_doc = frappe.get_doc(
{
"doctype": "DocType",
"name": TestCase.tree_doctype_name,
"module": "Custom",
"custom": 1,
"is_tree": 1,
"autoname": "field:random",
"fields": [{"fieldname": "random", "label": "Random", "fieldtype": "Data"}],
}
).insert()
TestCase.tree_doc.search_fields = "parent_test_tree_order"
TestCase.tree_doc.save()
if not frappe.db.exists("DocType", TestCase.tree_doctype_name):
TestCase.tree_doc = frappe.get_doc(
{
"doctype": "DocType",
"name": TestCase.tree_doctype_name,
"module": "Custom",
"custom": 1,
"is_tree": 1,
"autoname": "field:random",
"fields": [{"fieldname": "random", "label": "Random", "fieldtype": "Data"}],
}
).insert()
TestCase.tree_doc.search_fields = "parent_test_tree_order"
TestCase.tree_doc.save()
else:
TestCase.tree_doc = frappe.get_doc("DocType", TestCase.tree_doctype_name)
# Create root for the tree doctype
frappe.get_doc(
{"doctype": TestCase.tree_doctype_name, "random": TestCase.parent_doctype_name, "is_group": 1}
).insert()
if not frappe.db.exists(TestCase.tree_doctype_name, {"random": TestCase.parent_doctype_name}):
frappe.get_doc(
{"doctype": TestCase.tree_doctype_name, "random": TestCase.parent_doctype_name, "is_group": 1}
).insert(ignore_if_duplicate=True)
# Create children for the root
for child_name in TestCase.child_doctypes_names:
@ -173,7 +174,7 @@ def setup_test_link_field_order(TestCase):
"random": child_name,
"parent_test_tree_order": TestCase.parent_doctype_name,
}
).insert()
).insert(ignore_if_duplicate=True)
TestCase.child_doctype_list.append(temp)

View file

@ -972,4 +972,5 @@ class TestTBSanitization(FrappeTestCase):
traceback = frappe.get_traceback(with_context=True)
self.assertNotIn("42", traceback)
self.assertIn("********", traceback)
self.assertIn("password =", traceback)
self.assertIn("safe_value", traceback)

View file

@ -3,6 +3,7 @@ import datetime
import signal
import unittest
from contextlib import contextmanager
from typing import Sequence
import frappe
from frappe.model.base_document import BaseDocument
@ -39,6 +40,10 @@ class FrappeTestCase(unittest.TestCase):
return super().setUpClass()
def assertSequenceSubset(self, larger: Sequence, smaller: Sequence, msg=None):
"""Assert that `expected` is a subset of `actual`."""
self.assertTrue(set(smaller).issubset(set(larger)), msg=msg)
# --- Frappe Framework specific assertions
def assertDocumentEqual(self, expected, actual):
"""Compare a (partial) expected document with actual Document."""

File diff suppressed because it is too large Load diff

View file

@ -346,7 +346,7 @@ def _get_traceback_sanitizer():
return Format(
custom_var_printers=[
# redact variables
*[(variable_name, lambda: placeholder) for variable_name in blocklist],
*[(variable_name, lambda *a, **kw: placeholder) for variable_name in blocklist],
# redact dictionary keys
(["_secret", dict, lambda *a, **kw: False], dict_printer),
],

View file

@ -153,6 +153,7 @@ def run_doc_method(doctype, name, doc_method, **kwargs):
def execute_job(site, method, event, job_name, kwargs, user=None, is_async=True, retry=0):
"""Executes job in a worker, performs commit/rollback and logs if there is any error"""
retval = None
if is_async:
frappe.connect(site)
if os.environ.get("CI"):
@ -167,9 +168,11 @@ def execute_job(site, method, event, job_name, kwargs, user=None, is_async=True,
else:
method_name = cstr(method.__name__)
frappe.monitor.start("job", method_name, kwargs)
for before_job_task in frappe.get_hooks("before_job"):
frappe.call(before_job_task, method=method_name, kwargs=kwargs, transaction_type="job")
try:
method(**kwargs)
retval = method(**kwargs)
except (frappe.db.InternalError, frappe.RetryBackgroundJobError) as e:
frappe.db.rollback()
@ -200,14 +203,12 @@ def execute_job(site, method, event, job_name, kwargs, user=None, is_async=True,
else:
frappe.db.commit()
return retval
finally:
# background job hygiene: release file locks if unreleased
# if this breaks something, move it to failed jobs alone - gavin@frappe.io
for doc in frappe.local.locked_documents:
doc.unlock()
for after_job_task in frappe.get_hooks("after_job"):
frappe.call(after_job_task, method=method_name, kwargs=kwargs, result=retval)
frappe.monitor.stop()
if is_async:
frappe.destroy()

View file

@ -458,6 +458,15 @@ app_license = "{app_license}"
# ignore_links_on_delete = ["Communication", "ToDo"]
# Request Events
# ----------------
# before_request = ["{app_name}.utils.before_request"]
# after_request = ["{app_name}.utils.after_request"]
# Job Events
# ----------
# before_job = ["{app_name}.utils.before_job"]
# after_job = ["{app_name}.utils.after_job"]
# User Data Protection
# --------------------

View file

@ -11,7 +11,7 @@ Use `frappe.utils.synchroniztion.filelock` for process synchroniztion.
import os
from time import time
from frappe import _
import frappe
from frappe.utils import get_site_path, touch_file
LOCKS_DIR = "locks"
@ -62,3 +62,9 @@ def get_lock_path(name):
name = name.lower()
lock_path = get_site_path(LOCKS_DIR, name + ".lock")
return lock_path
def release_document_locks():
"""Unlocks all documents that were locked by the current context."""
for doc in frappe.local.locked_documents:
doc.unlock()

View file

@ -4,6 +4,7 @@ import re
from bleach_allowlist import bleach_allowlist
import frappe
from frappe.utils.data import escape_html
EMOJI_PATTERN = re.compile(
"(\ud83d[\ude00-\ude4f])|"
@ -204,10 +205,12 @@ def get_icon_html(icon, small=False):
if is_image(icon):
return (
f'<img style="width: 16px; height: 16px;" src="{icon}">' if small else f'<img src="{icon}">'
f"<img style='width: 16px; height: 16px;' src={escape_html(icon)!r}>"
if small
else f"<img src={escape_html(icon)!r}>"
)
else:
return f"<i class='{icon}'></i>"
return f"<i class={escape_html(icon)!r}></i>"
def unescape_html(value):

View file

@ -92,31 +92,35 @@ def enqueue_events(site: str) -> list[str] | None:
return enqueued_jobs
def is_scheduler_inactive() -> bool:
def is_scheduler_inactive(verbose=True) -> bool:
if frappe.local.conf.maintenance_mode:
cprint(f"{frappe.local.site}: Maintenance mode is ON")
if verbose:
cprint(f"{frappe.local.site}: Maintenance mode is ON")
return True
if frappe.local.conf.pause_scheduler:
cprint(f"{frappe.local.site}: frappe.conf.pause_scheduler is SET")
if verbose:
cprint(f"{frappe.local.site}: frappe.conf.pause_scheduler is SET")
return True
if is_scheduler_disabled():
if is_scheduler_disabled(verbose=verbose):
return True
return False
def is_scheduler_disabled() -> bool:
def is_scheduler_disabled(verbose=True) -> bool:
if frappe.conf.disable_scheduler:
cprint(f"{frappe.local.site}: frappe.conf.disable_scheduler is SET")
if verbose:
cprint(f"{frappe.local.site}: frappe.conf.disable_scheduler is SET")
return True
scheduler_disabled = not frappe.utils.cint(
frappe.db.get_single_value("System Settings", "enable_scheduler")
)
if scheduler_disabled:
cprint(f"{frappe.local.site}: SystemSettings.enable_scheduler is UNSET")
if verbose:
cprint(f"{frappe.local.site}: SystemSettings.enable_scheduler is UNSET")
return scheduler_disabled

View file

@ -1,35 +0,0 @@
import json
import requests
import frappe
@frappe.whitelist()
def remote_login():
try:
login_url = frappe.conf.subscription["login_url"]
if login_url:
resp = requests.post(login_url)
if resp.status_code != 200:
return
return json.loads(resp.text)["message"]
except Exception:
return False
return False
def enable_manage_subscription():
if not frappe.db.exists("Navbar Item", {"item_label": "Manage Subscriptions"}):
return
navbar_item, hidden = frappe.db.get_value(
"Navbar Item", {"item_label": "Manage Subscriptions"}, ["name", "hidden"]
)
if navbar_item and hidden:
doc = frappe.get_cached_doc("Navbar Item", navbar_item)
doc.hidden = False
doc.save()

View file

@ -7,6 +7,8 @@ frappe.ui.form.on("Blog Post", {
frm.set_df_property("hide_cta", "hidden", !value);
});
frm.trigger("add_publish_button");
generate_google_search_preview(frm);
},
title: function (frm) {
@ -30,6 +32,12 @@ frappe.ui.form.on("Blog Post", {
});
}
},
add_publish_button(frm) {
frm.add_custom_button(frm.doc.published ? __("Unpublish") : __("Publish"), () => {
frm.set_value("published", !frm.doc.published);
frm.save();
});
},
});
function generate_google_search_preview(frm) {

View file

@ -53,6 +53,7 @@
"default": "0",
"fieldname": "published",
"fieldtype": "Check",
"hidden": 1,
"label": "Published"
},
{
@ -215,7 +216,7 @@
"is_published_field": "published",
"links": [],
"make_attachments_public": 1,
"modified": "2022-10-18 10:09:10.550734",
"modified": "2023-02-17 11:31:32.223524",
"modified_by": "Administrator",
"module": "Website",
"name": "Blog Post",

View file

@ -6,9 +6,9 @@ import frappe.utils
from frappe import _
from frappe.auth import LoginManager
from frappe.integrations.doctype.ldap_settings.ldap_settings import LDAPSettings
from frappe.integrations.oauth2_logins import decoder_compat
from frappe.rate_limiter import rate_limit
from frappe.utils import cint, get_url
from frappe.utils.data import escape_html
from frappe.utils.html_utils import get_icon_html
from frappe.utils.jinja import guess_is_path
from frappe.utils.oauth import get_oauth2_authorize_url, get_oauth_keys, redirect_post_login
@ -72,7 +72,7 @@ def get_context(context):
if provider.provider_name == "Custom":
icon = get_icon_html(provider.icon, small=True)
else:
icon = f"<img src='{provider.icon}' alt={provider.provider_name}>"
icon = f"<img src={escape_html(provider.icon)!r} alt={escape_html(provider.provider_name)!r}>"
if provider.client_id and provider.base_url and get_oauth_keys(provider.name):
context.provider_logins.append(

View file

@ -31,12 +31,12 @@ dependencies = [
"cairocffi==1.2.0",
"chardet~=4.0.0",
"croniter~=1.3.5",
"cryptography~=38.0.3",
"cryptography~=39.0.1",
"email-reply-parser~=0.5.12",
"git-url-parse~=1.2.2",
"gunicorn~=20.1.0",
"html5lib~=1.1",
"ipython~=8.4.0",
"ipython~=8.10.0",
"ldap3~=2.9",
"markdown2~=2.4.0",
"MarkupSafe>=2.1.0,<3",
@ -50,7 +50,7 @@ dependencies = [
"premailer~=3.8.0",
"psutil~=5.9.1",
"psycopg2-binary~=2.9.1",
"pyOpenSSL~=22.1.0",
"pyOpenSSL~=23.0.0",
"pycryptodome~=3.10.1",
"pydantic~=1.10.2",
"pyotp~=2.6.0",