Merge branch 'develop' into fix-note-2
This commit is contained in:
commit
97dde45067
61 changed files with 1437 additions and 1182 deletions
2
.github/workflows/linters.yml
vendored
2
.github/workflows/linters.yml
vendored
|
|
@ -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 .
|
||||
|
|
|
|||
20
.mergify.yml
20
.mergify.yml
|
|
@ -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 }}"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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 []
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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....")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 .*")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# --------------------
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue