Merge branch 'develop' into feat-number-card-workspace-dev

This commit is contained in:
Shariq Ansari 2023-02-28 10:57:38 +05:30 committed by GitHub
commit 863987e6bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
91 changed files with 1259 additions and 620 deletions

View file

@ -273,4 +273,29 @@ context("Form Builder", () => {
.find(".msgprint")
.should("contain", "cannot be hidden and mandatory without any default value");
});
it("Undo/Redo", () => {
cy.visit(`/app/form-builder/${doctype_name}`);
// click on second tab
cy.get(".tabs .tab:last").click();
let first_column = ".tab-content.active .section-columns-container:first .column:first";
let first_field = first_column + " .field:first";
let label = "div[title='Double click to edit label'] span:first";
// drag the first field to second position
cy.get(first_field).drag(first_column + " .field:nth-child(2)", {
target: { x: 100, y: 10 },
});
cy.get(first_field).find(label).should("have.text", "Check");
// undo
cy.get("body").type("{ctrl}z");
cy.get(first_field).find(label).should("have.text", "Data");
// redo
cy.get("body").type("{ctrl}{shift}z");
cy.get(first_field).find(label).should("have.text", "Check");
});
});

View file

@ -15,11 +15,13 @@ context("List View Settings", () => {
cy.clear_filters();
cy.wait(300);
cy.get(".list-count").should("contain", "20 of");
cy.get("[href='#icon-small-message']").should("be.visible");
cy.get(".menu-btn-group button").click();
cy.get(".dropdown-menu li").filter(":visible").contains("List Settings").click();
cy.get(".modal-dialog").should("contain", "DocType Settings");
cy.findByLabelText("Disable Count").check({ force: true });
cy.findByLabelText("Disable Comment Count").check({ force: true });
cy.findByLabelText("Disable Sidebar Stats").check({ force: true });
cy.findByRole("button", { name: "Save" }).click();
@ -27,11 +29,13 @@ context("List View Settings", () => {
cy.get(".list-count").should("be.empty");
cy.get(".list-sidebar .list-tags").should("not.exist");
cy.get("[href='#icon-small-message']").should("not.be.visible");
cy.get(".menu-btn-group button").click({ force: true });
cy.get(".dropdown-menu li").filter(":visible").contains("List Settings").click();
cy.get(".modal-dialog").should("contain", "DocType Settings");
cy.findByLabelText("Disable Count").uncheck({ force: true });
cy.findByLabelText("Disable Comment Count").uncheck({ force: true });
cy.findByLabelText("Disable Sidebar Stats").uncheck({ force: true });
cy.findByRole("button", { name: "Save" }).click();
});

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

View file

@ -0,0 +1,8 @@
// Copyright (c) 2023, Frappe Technologies and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Reminder", {
// refresh(frm) {
// },
// });

View file

@ -0,0 +1,90 @@
{
"actions": [],
"autoname": "hash",
"creation": "2023-02-22 11:23:58.183276",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"user",
"remind_at",
"description",
"reminder_doctype",
"reminder_docname",
"notified"
],
"fields": [
{
"default": "__user",
"fieldname": "user",
"fieldtype": "Link",
"hidden": 1,
"label": "User",
"options": "User",
"reqd": 1,
"search_index": 1
},
{
"fieldname": "reminder_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Document Type",
"options": "DocType",
"read_only": 1
},
{
"fieldname": "reminder_docname",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Document Name",
"options": "reminder_doctype",
"read_only": 1
},
{
"default": "now",
"fieldname": "remind_at",
"fieldtype": "Datetime",
"in_list_view": 1,
"label": "Remind At",
"reqd": 1,
"search_index": 1
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description",
"reqd": 1
},
{
"default": "0",
"fieldname": "notified",
"fieldtype": "Check",
"hidden": 1,
"label": "notified"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-02-24 13:47:50.419648",
"modified_by": "Administrator",
"module": "Automation",
"name": "Reminder",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"if_owner": 1,
"read": 1,
"role": "All",
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "description"
}

View file

@ -0,0 +1,78 @@
# Copyright (c) 2023, Frappe Technologies and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint
from frappe.utils.data import add_to_date, get_datetime, now_datetime
class Reminder(Document):
@staticmethod
def clear_old_logs(days=30):
from frappe.query_builder import Interval
from frappe.query_builder.functions import Now
table = frappe.qb.DocType("Reminder")
frappe.db.delete(table, filters=(table.remind_at < (Now() - Interval(days=days))))
def validate(self):
self.user = frappe.session.user
if get_datetime(self.remind_at) < now_datetime():
frappe.throw(_("Reminder cannot be created in past."))
def send_reminder(self):
if self.notified:
return
self.db_set("notified", 1, update_modified=False)
try:
notification = frappe.new_doc("Notification Log")
notification.for_user = self.user
notification.set("type", "Alert")
notification.document_type = self.reminder_doctype
notification.document_name = self.reminder_docname
notification.subject = self.description
notification.insert()
except Exception:
self.log_error("Failed to send reminder")
@frappe.whitelist()
def create_new_reminder(
remind_at: str,
description: str,
reminder_doctype: str | None = None,
reminder_docname: str | None = None,
):
reminder = frappe.new_doc("Reminder")
reminder.description = description
reminder.remind_at = remind_at
reminder.reminder_doctype = reminder_doctype
reminder.reminder_docname = reminder_docname
return reminder.insert()
def send_reminders():
# Ensure that we send all reminders that might be before next job execution.
job_freq = cint(frappe.get_conf().scheduler_interval) or 240
upper_threshold = add_to_date(now_datetime(), seconds=job_freq, as_string=True, as_datetime=True)
lower_threshold = add_to_date(now_datetime(), hours=-8, as_string=True, as_datetime=True)
pending_reminders = frappe.get_all(
"Reminder",
filters=[
("remind_at", "<=", upper_threshold),
("remind_at", ">=", lower_threshold), # dont send too old reminders if failed to send
("notified", "=", 0),
],
pluck="name",
)
for reminder in pending_reminders:
frappe.get_doc("Reminder", reminder).send_reminder()

View file

@ -0,0 +1,28 @@
# Copyright (c) 2023, Frappe Technologies and Contributors
# See license.txt
import frappe
from frappe.automation.doctype.reminder.reminder import create_new_reminder, send_reminders
from frappe.desk.doctype.notification_log.notification_log import get_notification_logs
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_to_date, now_datetime
class TestReminder(FrappeTestCase):
def test_reminder(self):
description = "TEST_REMINDER"
create_new_reminder(
remind_at=add_to_date(now_datetime(), minutes=1, as_datetime=True, as_string=True),
description=description,
)
send_reminders()
notifications = get_notification_logs()["notification_logs"]
self.assertIn(
description,
[n.subject for n in notifications],
msg=f"Failed to find reminder notification \n{notifications}",
)

View file

@ -12,6 +12,7 @@ from frappe.desk.doctype.route_history.route_history import frequently_visited_l
from frappe.desk.form.load import get_meta_bundle
from frappe.email.inbox import get_email_accounts
from frappe.model.base_document import get_controller
from frappe.permissions import has_permission
from frappe.query_builder import DocType
from frappe.query_builder.functions import Count
from frappe.query_builder.terms import ParameterizedValueWrapper, SubQuery
@ -234,6 +235,9 @@ def get_user_pages_or_reports(parent, cache=False):
has_role[p.name] = {"modified": p.modified, "title": p.title}
elif parent == "Report":
if not has_permission("Report", raise_exception=False):
return {}
reports = frappe.get_list(
"Report",
fields=["name", "report_type"],

View file

@ -204,7 +204,6 @@
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 1,
"share": 1,
"write": 1
}

View file

@ -53,7 +53,6 @@
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 1,
"share": 1,
"write": 1
}
@ -62,4 +61,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View file

@ -275,7 +275,6 @@
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 1,
"share": 1,
"write": 1
},

View file

@ -27,7 +27,6 @@
"report",
"export",
"import",
"set_user_permissions",
"column_break_19",
"share",
"print",
@ -179,13 +178,6 @@
"fieldtype": "Check",
"label": "Import"
},
{
"default": "0",
"description": "This role update User Permissions for a user",
"fieldname": "set_user_permissions",
"fieldtype": "Check",
"label": "Set User Permissions"
},
{
"fieldname": "column_break_19",
"fieldtype": "Column Break"
@ -223,7 +215,7 @@
}
],
"links": [],
"modified": "2020-12-03 15:20:48.296730",
"modified": "2023-02-20 13:19:04.889081",
"modified_by": "Administrator",
"module": "Core",
"name": "Custom DocPerm",

View file

@ -304,6 +304,7 @@
},
{
"default": "0",
"depends_on": "eval:!in_list(['Section Break', 'Column Break', 'Tab Break'], doc.fieldtype)",
"fieldname": "permlevel",
"fieldtype": "Int",
"label": "Perm Level",
@ -555,7 +556,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-01-11 20:46:43.164926",
"modified": "2023-02-20 12:07:29.552523",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",

View file

@ -26,7 +26,6 @@
"report",
"export",
"import",
"set_user_permissions",
"column_break_19",
"share",
"print",
@ -178,13 +177,6 @@
"fieldtype": "Check",
"label": "Import"
},
{
"default": "0",
"description": "This role update User Permissions for a user",
"fieldname": "set_user_permissions",
"fieldtype": "Check",
"label": "Set User Permissions"
},
{
"fieldname": "column_break_19",
"fieldtype": "Column Break"
@ -218,7 +210,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2020-12-03 15:15:30.488212",
"modified": "2023-02-20 13:21:45.071310",
"modified_by": "Administrator",
"module": "Core",
"name": "DocPerm",

View file

@ -4,5 +4,6 @@
# import frappe
{base_class_import}
class {classname}({base_class}):
{custom_controller}

View file

@ -55,7 +55,7 @@ frappe.ui.form.on("DocType", {
msg += __("If you just want to customize for your site, use {0} instead.", [
customize_form_link,
]);
frm.dashboard.add_comment(msg, "yellow");
frm.dashboard.add_comment(msg, "yellow", true);
}
if (frm.is_new()) {

View file

@ -1604,11 +1604,6 @@ def validate_permissions(doctype, for_remove=False, alert=False):
d.set("import", 0)
d.set("export", 0)
for ptype, label in [["set_user_permissions", _("Set User Permissions")]]:
if d.get(ptype):
d.set(ptype, 0)
frappe.msgprint(_("{0} cannot be set for Single types").format(label))
def check_if_submittable(d):
if d.submit and not issubmittable:
frappe.throw(_("{0}: Cannot set Assign Submit if not Submittable").format(get_txt(d)))

View file

@ -81,10 +81,10 @@
},
{
"depends_on": "transaction_type",
"description": "Generate 3 preview of names generate by any valid series.",
"description": "Get a preview of generated names with a series.",
"fieldname": "try_naming_series",
"fieldtype": "Data",
"label": "Try a naming Series"
"label": "Try a Naming Series"
},
{
"fieldname": "transaction_type",
@ -111,7 +111,7 @@
"icon": "fa fa-sort-by-order",
"issingle": 1,
"links": [],
"modified": "2022-05-30 23:51:36.136535",
"modified": "2023-02-20 13:11:56.662100",
"modified_by": "Administrator",
"module": "Core",
"name": "Document Naming Settings",
@ -130,4 +130,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View file

@ -19,6 +19,8 @@ DEFAULT_LOGTYPES_RETENTION = {
"Route History": 90,
"Submission Queue": 30,
"Prepared Report": 30,
"Webhook Request Log": 30,
"Reminder": 30,
}

View file

@ -769,7 +769,6 @@
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 1,
"share": 1,
"write": 1
},
@ -793,4 +792,4 @@
"states": [],
"title_field": "full_name",
"track_changes": 1
}
}

View file

@ -320,7 +320,6 @@ frappe.PermissionEngine = class PermissionEngine {
"report",
"import",
"export",
"set_user_permissions",
"share",
];
}

View file

@ -62,8 +62,8 @@ def get_roles_and_doctypes():
roles_list = [{"label": _(d.get("name")), "value": d.get("name")} for d in roles]
return {
"doctypes": sorted(doctypes_list, key=lambda d: d["label"]),
"roles": sorted(roles_list, key=lambda d: d["label"]),
"doctypes": sorted(doctypes_list, key=lambda d: d["label"].casefold()),
"roles": sorted(roles_list, key=lambda d: d["label"].casefold()),
}

View file

@ -18,6 +18,18 @@ class CustomField(Document):
self.name = self.dt + "-" + self.fieldname
def set_fieldname(self):
restricted = (
"name",
"parent",
"creation",
"modified",
"modified_by",
"parentfield",
"parenttype",
"file_list",
"flags",
"docstatus",
)
if not self.fieldname:
label = self.label
if not label:
@ -34,6 +46,9 @@ class CustomField(Document):
# fieldnames should be lowercase
self.fieldname = self.fieldname.lower()
if self.fieldname in restricted:
self.fieldname = self.fieldname + "1"
def before_insert(self):
self.set_fieldname()

View file

@ -314,22 +314,59 @@ frappe.ui.form.on("DocType State", {
},
});
frappe.customize_form.set_primary_action = function (frm) {
frm.page.set_primary_action(__("Update"), function () {
if (frm.doc.doc_type) {
return frm.call({
doc: frm.doc,
freeze: true,
btn: frm.page.btn_primary,
method: "save_customization",
callback: function (r) {
if (!r.exc) {
frappe.customize_form.clear_locals_and_refresh(frm);
frm.script_manager.trigger("doc_type");
}
},
});
frappe.customize_form.validate_fieldnames = async function (frm) {
for (let i = 0; i < frm.doc.fields.length; i++) {
let field = frm.doc.fields[i];
let fieldname = field.label && frappe.model.scrub(field.label).toLowerCase();
if (
field.label &&
!field.fieldname &&
in_list(frappe.model.restricted_fields, fieldname)
) {
let message = __(
"For field <b>{0}</b> in row <b>{1}</b>, fieldname <b>{2}</b> is restricted it will be renamed as <b>{2}1</b>. Do you want to continue?",
[field.label, field.idx, fieldname]
);
await pause_to_confirm(message);
}
}
function pause_to_confirm(message) {
return new Promise((resolve) => {
frappe.confirm(
message,
() => resolve(),
() => {
frm.page.btn_primary.prop("disabled", false);
}
);
});
}
};
frappe.customize_form.save_customization = function (frm) {
if (frm.doc.doc_type) {
return frm.call({
doc: frm.doc,
freeze: true,
freeze_message: __("Saving Customization..."),
btn: frm.page.btn_primary,
method: "save_customization",
callback: function (r) {
if (!r.exc) {
frappe.customize_form.clear_locals_and_refresh(frm);
frm.script_manager.trigger("doc_type");
}
},
});
}
};
frappe.customize_form.set_primary_action = function (frm) {
frm.page.set_primary_action(__("Update"), async () => {
await this.validate_fieldnames(frm);
this.save_customization(frm);
});
};

View file

@ -212,6 +212,7 @@
},
{
"default": "0",
"depends_on": "eval:!in_list(['Section Break', 'Column Break', 'Tab Break'], doc.fieldtype)",
"fieldname": "permlevel",
"fieldtype": "Int",
"in_list_view": 1,
@ -467,7 +468,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-11-30 14:25:50.649449",
"modified": "2023-02-20 12:07:40.242470",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",

View file

@ -57,7 +57,6 @@
"report": 1,
"export": 1,
"import": 0,
"set_user_permissions": 0,
"share": 1,
"print": 1,
"email": 1,

View file

@ -57,7 +57,6 @@
"report": 1,
"export": 1,
"import": 0,
"set_user_permissions": 0,
"share": 1,
"print": 1,
"email": 1,

View file

@ -233,7 +233,7 @@ class Database:
elif self.is_read_only_mode_error(e):
frappe.throw(
_(
"Site is running in read only mode, this action can not be performed right now. Please try again later."
"Site is running in read only mode for maintenance or site update, this action can not be performed right now. Please try again later."
),
title=_("In Read Only Mode"),
exc=frappe.InReadOnlyMode,
@ -1046,7 +1046,7 @@ class Database:
dt = dt.copy() # don't modify the original dict
dt, dn = dt.pop("doctype"), dt
return self.get_value(dt, dn, ignore=True, cache=cache)
return self.get_value(dt, dn, ignore=True, cache=cache, order_by=None)
def count(self, dt, filters=None, debug=False, cache=False, distinct: bool = True):
"""Returns `COUNT(*)` for given DocType and filters."""

View file

@ -278,15 +278,14 @@ def get_group_by_chart_config(chart, filters):
group_by_field = chart.group_by_based_on
doctype = chart.document_type
data = frappe.db.get_list(
data = frappe.get_list(
doctype,
fields=[
f"{group_by_field} as name",
"{aggregate_function}({value_field}) as count".format(
aggregate_function=aggregate_function, value_field=value_field
),
f"{aggregate_function}({value_field}) as count",
],
filters=filters,
parent_doctype=chart.parent_document_type,
group_by=group_by_field,
order_by="count desc",
ignore_ifnull=True,

View file

@ -7,6 +7,7 @@
"engine": "InnoDB",
"field_order": [
"disable_count",
"disable_comment_count",
"disable_sidebar_stats",
"disable_auto_refresh",
"total_fields",
@ -49,13 +50,20 @@
"hidden": 1,
"label": "Fields",
"read_only": 1
},
{
"default": "0",
"fieldname": "disable_comment_count",
"fieldtype": "Check",
"label": "Disable Comment Count"
}
],
"links": [],
"modified": "2020-05-12 18:27:15.568199",
"modified": "2023-02-14 14:46:43.764229",
"modified_by": "Administrator",
"module": "Desk",
"name": "List View Settings",
"naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
@ -72,5 +80,6 @@
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View file

@ -92,4 +92,7 @@ def get_permission_query_conditions(user):
@frappe.whitelist()
def set_seen_value(value, user):
if frappe.flags.read_only:
return
frappe.db.set_value("Notification Settings", user, "seen", value, update_modified=False)

View file

@ -59,7 +59,7 @@ def get_tags(doctype, txt):
tag = frappe.get_list("Tag", filters=[["name", "like", f"%{txt}%"]])
tags = [t.name for t in tag]
return sorted(filter(lambda t: t and txt.lower() in t.lower(), list(set(tags))))
return sorted(filter(lambda t: t and txt.casefold() in t.casefold(), list(set(tags))))
class DocTags:

View file

@ -282,7 +282,7 @@ def scrub_custom_query(query, key, txt):
def relevance_sorter(key, query, as_dict):
value = _(key.name if as_dict else key[0])
return (cstr(value).lower().startswith(query.lower()) is not True, value)
return (cstr(value).casefold().startswith(query.casefold()) is not True, value)
def validate_and_sanitize_search_inputs(fn):

View file

@ -628,7 +628,6 @@
"delete": 1,
"read": 1,
"role": "System Manager",
"set_user_permissions": 1,
"write": 1
},
{
@ -640,4 +639,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View file

@ -657,8 +657,6 @@ class EmailAccount(Document):
if not email_server:
return
email_server.connect()
if email_server.imap:
try:
message = safe_encode(message)

View file

@ -145,7 +145,6 @@
"delete": 1,
"read": 1,
"role": "System Manager",
"set_user_permissions": 1,
"share": 1,
"write": 1
}
@ -154,4 +153,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View file

@ -251,7 +251,6 @@
"read": 1,
"report": 1,
"role": "Newsletter Manager",
"set_user_permissions": 1,
"share": 1,
"write": 1
}
@ -261,4 +260,4 @@
"sort_order": "ASC",
"title_field": "subject",
"track_changes": 1
}
}

View file

@ -244,6 +244,10 @@ class InReadOnlyMode(ValidationError):
http_status_code = 503 # temporarily not available
class SessionBootFailed(ValidationError):
http_status_code = 500
class TooManyWritesError(Exception):
pass

View file

@ -69,7 +69,6 @@
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 1,
"share": 1,
"write": 1
},
@ -87,4 +86,4 @@
"states": [],
"track_changes": 1,
"translated_doctype": 1
}
}

View file

@ -194,6 +194,7 @@ scheduler_events = {
"frappe.email.doctype.email_account.email_account.notify_unreplied",
"frappe.utils.global_search.sync_global_search",
"frappe.monitor.flush",
"frappe.automation.doctype.reminder.reminder.send_reminders",
],
"hourly": [
"frappe.model.utils.link_count.update_link_count",
@ -392,4 +393,22 @@ ignore_links_on_delete = [
"Document Share Key",
"Integration Request",
"Unhandled Email",
"Webhook Request Log",
]
# 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

@ -422,7 +422,10 @@ def insert_event_in_google_calendar(doc, method=None):
event = (
google_calendar.events()
.insert(
calendarId=doc.google_calendar_id, body=event, conferenceDataVersion=conference_data_version
calendarId=doc.google_calendar_id,
body=event,
conferenceDataVersion=conference_data_version,
sendUpdates="all",
)
.execute()
)
@ -504,6 +507,7 @@ def update_event_in_google_calendar(doc, method=None):
eventId=doc.google_calendar_event_id,
body=event,
conferenceDataVersion=conference_data_version,
sendUpdates="all",
)
.execute()
)

View file

@ -128,32 +128,40 @@ def enqueue_webhook(doc, webhook) -> None:
)
r.raise_for_status()
frappe.logger().debug({"webhook_success": r.text})
log_request(webhook.request_url, headers, data, r)
log_request(webhook.name, doc.name, webhook.request_url, headers, data, r)
break
except requests.exceptions.ReadTimeout as e:
frappe.logger().debug({"webhook_error": e, "try": i + 1})
log_request(webhook.request_url, headers, data)
log_request(webhook.name, doc.name, webhook.request_url, headers, data)
except Exception as e:
frappe.logger().debug({"webhook_error": e, "try": i + 1})
log_request(webhook.request_url, headers, data, r)
log_request(webhook.name, doc.name, webhook.request_url, headers, data, r)
sleep(3 * i + 1)
if i != 2:
continue
else:
webhook.log_error("Webhook failed")
def log_request(url: str, headers: dict, data: dict, res: requests.Response | None = None):
def log_request(
webhook: str,
docname: str,
url: str,
headers: dict,
data: dict,
res: requests.Response | None = None,
):
request_log = frappe.get_doc(
{
"doctype": "Webhook Request Log",
"webhook": webhook,
"reference_document": docname,
"user": frappe.session.user if frappe.session.user else None,
"url": url,
"headers": frappe.as_json(headers) if headers else None,
"data": frappe.as_json(data) if data else None,
"response": frappe.as_json(res.json()) if res else None,
"error": frappe.get_traceback(),
}
)

View file

@ -7,11 +7,14 @@
"engine": "InnoDB",
"field_order": [
"user",
"webhook",
"reference_document",
"headers",
"data",
"column_break_4",
"url",
"response"
"response",
"error"
],
"fields": [
{
@ -51,12 +54,32 @@
"label": "User",
"options": "User",
"read_only": 1
},
{
"fieldname": "reference_document",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Reference Document",
"read_only": 1
},
{
"fieldname": "error",
"fieldtype": "Text",
"label": "Error",
"read_only": 1
},
{
"fieldname": "webhook",
"fieldtype": "Link",
"label": "Webhook",
"options": "Webhook"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-05-03 09:33:49.240777",
"modified": "2023-02-24 14:59:24.743552",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Webhook Request Log",

View file

@ -1,9 +1,15 @@
# Copyright (c) 2021, Frappe Technologies and contributors
# License: MIT. See LICENSE
# import frappe
import frappe
from frappe.model.document import Document
class WebhookRequestLog(Document):
pass
@staticmethod
def clear_old_logs(days=30):
from frappe.query_builder import Interval
from frappe.query_builder.functions import Now
table = frappe.qb.DocType("Webhook Request Log")
frappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days))))

View file

@ -29,6 +29,7 @@ from frappe.utils import (
get_timespan_date_range,
make_filter_tuple,
)
from frappe.utils.data import sbool
LOCATE_PATTERN = re.compile(r"locate\([^,]+,\s*[`\"]?name[`\"]?\s*\)", flags=re.IGNORECASE)
LOCATE_CAST_PATTERN = re.compile(
@ -74,6 +75,10 @@ class DatabaseQuery:
self._doctype_meta = frappe.get_meta(self.doctype)
return self._doctype_meta
@property
def query_tables(self):
return self.tables + [d.table_name for d in self.link_tables]
def execute(
self,
fields=None,
@ -196,7 +201,7 @@ class DatabaseQuery:
result = self.build_and_run()
if with_comment_count and not as_list and self.doctype:
if sbool(with_comment_count) and not as_list and self.doctype:
self.add_comment_count(result)
if save_user_settings:
@ -472,9 +477,7 @@ class DatabaseQuery:
table_name = table_name[13:]
if not table_name[0] == "`":
table_name = f"`{table_name}`"
if table_name not in self.tables and table_name not in (
d.table_name for d in self.link_tables
):
if table_name not in self.query_tables:
self.append_table(table_name)
def append_table(self, table_name):
@ -640,7 +643,7 @@ class DatabaseQuery:
table, column = column.split(".", 1)
ch_doctype = table.replace("`", "").replace("tab", "", 1)
if wrap_grave_quotes(table) in self.tables:
if wrap_grave_quotes(table) in self.query_tables:
permitted_child_table_fields = get_permitted_fields(
doctype=ch_doctype, parenttype=self.doctype
)

View file

@ -676,7 +676,11 @@ class Document(BaseDocument):
for df in self.meta.fields:
if df.permlevel and hasattr(self, df.fieldname) and df.permlevel not in has_access_to:
delattr(self, df.fieldname)
try:
delattr(self, df.fieldname)
except AttributeError:
# hasattr might return True for class attribute which can't be delattr-ed.
continue
for table_field in self.meta.get_table_fields():
for df in frappe.get_meta(table_field.options).fields or []:

View file

@ -22,7 +22,6 @@ rights = (
"report",
"import",
"export",
"set_user_permissions",
"share",
)
@ -459,29 +458,6 @@ def get_doctypes_with_custom_docperms():
return [d.parent for d in doctypes]
def can_set_user_permissions(doctype, docname=None):
# System Manager can always set user permissions
if frappe.session.user == "Administrator" or "System Manager" in frappe.get_roles():
return True
meta = frappe.get_meta(doctype)
# check if current user has read permission for docname
if docname and not has_permission(doctype, "read", docname):
return False
# check if current user has a role that can set permission
if get_role_permissions(meta).set_user_permissions != 1:
return False
return True
def set_user_permission_if_allowed(doctype, name, user, with_message=False):
if get_role_permissions(frappe.get_meta(doctype), user).set_user_permissions != 1:
add_user_permission(doctype, name, user)
def add_user_permission(
doctype,
name,

View file

@ -1,145 +1,143 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:print_heading",
"beta": 0,
"creation": "2013-01-10 16:34:24",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 0,
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:print_heading",
"beta": 0,
"creation": "2013-01-10 16:34:24",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 0,
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "print_heading",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Print Heading",
"length": 0,
"no_copy": 0,
"oldfieldname": "print_heading",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"allow_bulk_edit": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "print_heading",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Print Heading",
"length": 0,
"no_copy": 0,
"oldfieldname": "print_heading",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"oldfieldname": "description",
"oldfieldtype": "Small Text",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"oldfieldname": "description",
"oldfieldtype": "Small Text",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "300px"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-font",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-05-03 05:59:09.131569",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Heading",
"owner": "Administrator",
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-font",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-05-03 05:59:09.131569",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Heading",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 0,
"write": 1
},
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "All",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "All",
"share": 0,
"submit": 0,
"write": 0
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"search_fields": "print_heading",
"show_name_in_global_search": 0,
"sort_order": "DESC",
"track_changes": 0,
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"search_fields": "print_heading",
"show_name_in_global_search": 0,
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0
}
}

View file

@ -85,7 +85,7 @@ function delete_column(with_children) {
// remove column
columns.splice(index, 1);
store.selected_field = null;
store.form.selected_field = null;
}
function move_columns_to_section() {
@ -101,7 +101,7 @@ function move_columns_to_section() {
store.selected(column.df.name) ? 'selected' : ''
]"
:title="column.df.fieldname"
@click.stop="store.selected_field = column.df"
@click.stop="store.form.selected_field = column.df"
@mouseover.stop="hovered = true"
@mouseout.stop="hovered = false"
>

View file

@ -19,7 +19,7 @@ function remove_field() {
}
let index = props.column.fields.indexOf(props.field);
props.column.fields.splice(index, 1);
store.selected_field = null;
store.form.selected_field = null;
}
function move_fields_to_column() {
@ -51,7 +51,7 @@ function duplicate_field() {
// push duplicate_field after props.field in the same column
let index = props.column.fields.indexOf(props.field);
props.column.fields.splice(index + 1, 0, duplicate_field);
store.selected_field = duplicate_field.df;
store.form.selected_field = duplicate_field.df;
}
</script>
@ -63,7 +63,7 @@ function duplicate_field() {
store.selected(field.df.name) ? 'selected' : ''
]"
:title="field.df.fieldname"
@click.stop="store.selected_field = field.df"
@click.stop="store.form.selected_field = field.df"
@mouseover.stop="hovered = true"
@mouseout.stop="hovered = false"
>

View file

@ -14,18 +14,18 @@ let docfield_df = computed(() => {
if (in_list(frappe.model.layout_fields, df.fieldtype) || df.hidden) {
return false;
}
if (df.depends_on && !evaluate_depends_on_value(df.depends_on, store.selected_field)) {
if (df.depends_on && !evaluate_depends_on_value(df.depends_on, store.form.selected_field)) {
return false;
}
if (
in_list(["fetch_from", "fetch_if_empty"], df.fieldname) &&
in_list(frappe.model.no_value_type, store.selected_field.fieldtype)
in_list(frappe.model.no_value_type, store.form.selected_field.fieldtype)
) {
return false;
}
if (df.fieldname === "reqd" && store.selected_field.fieldtype === "Check") {
if (df.fieldname === "reqd" && store.form.selected_field.fieldtype === "Check") {
return false;
}
@ -34,11 +34,11 @@ let docfield_df = computed(() => {
df.options = "";
args.value = {};
if (in_list(["Table", "Link"], store.selected_field.fieldtype)) {
if (in_list(["Table", "Link"], store.form.selected_field.fieldtype)) {
df.fieldtype = "Link";
df.options = "DocType";
if (store.selected_field.fieldtype === "Table") {
if (store.form.selected_field.fieldtype === "Table") {
args.value.is_table_field = 1;
}
}
@ -63,14 +63,14 @@ let docfield_df = computed(() => {
<template>
<SearchBox v-model="search_text" />
<div class="control-data">
<div v-if="store.selected_field">
<div v-if="store.form.selected_field">
<div class="field" v-for="(df, i) in docfield_df" :key="i">
<component
:is="df.fieldtype.replace(' ', '') + 'Control'"
:args="args"
:df="df"
:value="store.selected_field[df.fieldname]"
v-model="store.selected_field[df.fieldname]"
:value="store.form.selected_field[df.fieldname]"
v-model="store.form.selected_field[df.fieldname]"
:data-fieldname="df.fieldname"
:data-fieldtype="df.fieldtype"
/>

View file

@ -8,11 +8,11 @@ import { onClickOutside, useMagicKeys, whenever } from "@vueuse/core";
let store = useStore();
let should_render = computed(() => {
return Object.keys(store.layout).length !== 0;
return Object.keys(store.form.layout).length !== 0;
});
let container = ref(null);
onClickOutside(container, () => store.selected_field = null);
onClickOutside(container, () => store.form.selected_field = null);
// cmd/ctrl + s to save the form
const { meta_s, ctrl_s } = useMagicKeys();
@ -53,7 +53,7 @@ function setup_change_doctype_dialog() {
}
watch(
() => store.layout,
() => store.form.layout,
() => (store.dirty = true),
{ deep: true }
);
@ -69,7 +69,7 @@ onMounted(() => {
v-if="should_render"
ref="container"
class="form-builder-container"
@click="store.selected_field = null"
@click="store.form.selected_field = null"
>
<div class="form-controls" @click.stop>
<div class="form-sidebar">

View file

@ -67,21 +67,21 @@ function delete_section(with_children) {
// remove section
sections.splice(index, 1);
store.selected_field = null;
store.form.selected_field = null;
}
function select_section() {
if (props.section.df.collapsible) {
collapsed.value = !collapsed.value;
}
store.selected_field = props.section.df;
store.form.selected_field = props.section.df;
}
function move_sections_to_tab() {
let new_tab = move_children_to_parent(props, "tab", "section", store.layout);
let new_tab = move_children_to_parent(props, "tab", "section", store.form.layout);
// activate tab
store.active_tab = new_tab;
store.form.active_tab = new_tab;
}
</script>

View file

@ -34,7 +34,7 @@ function resize(e) {
}
watch(
() => store.selected_field,
() => store.form.selected_field,
value => {
active_tab.value = value ? tab_titles[1] : tab_titles[0];
},

View file

@ -9,13 +9,12 @@ import { ref, computed, nextTick } from "vue";
let store = useStore();
let dragged = ref(false);
let layout = computed(() => store.layout);
let has_tabs = computed(() => layout.value.tabs.length > 1);
store.active_tab = layout.value.tabs[0].df.name;
let has_tabs = computed(() => store.form.layout.tabs.length > 1);
store.form.active_tab = store.form.layout.tabs[0].df.name;
function activate_tab(tab) {
store.active_tab = tab.df.name;
store.selected_field = tab.df;
store.form.active_tab = tab.df.name;
store.form.selected_field = tab.df;
// scroll to active tab
nextTick(() => {
@ -29,24 +28,24 @@ function activate_tab(tab) {
function drag_over(tab) {
!dragged.value &&
setTimeout(() => {
store.active_tab = tab.df.name;
store.form.active_tab = tab.df.name;
}, 500);
}
function add_new_tab() {
let tab = {
df: store.get_df("Tab Break", "", "Tab " + (layout.value.tabs.length + 1)),
df: store.get_df("Tab Break", "", "Tab " + (store.form.layout.tabs.length + 1)),
sections: [section_boilerplate()],
};
layout.value.tabs.push(tab);
store.form.layout.tabs.push(tab);
activate_tab(tab);
}
function add_new_section() {
let section = section_boilerplate();
store.current_tab.sections.push(section);
store.selected_field = section.df;
store.form.selected_field = section.df;
}
function is_current_tab_empty() {
@ -77,7 +76,7 @@ function remove_tab() {
}
function delete_tab(with_children) {
let tabs = layout.value.tabs;
let tabs = store.form.layout.tabs;
let index = tabs.indexOf(store.current_tab);
if (!with_children) {
@ -103,17 +102,17 @@ function delete_tab(with_children) {
// activate previous tab
let prev_tab_index = index == 0 ? 0 : index - 1;
store.active_tab = tabs[prev_tab_index].df.name;
store.selected_field = null;
store.form.active_tab = tabs[prev_tab_index].df.name;
store.form.selected_field = null;
}
</script>
<template>
<div class="tab-header" v-if="!(layout.tabs.length == 1 && store.read_only)">
<div class="tab-header" v-if="!(store.form.layout.tabs.length == 1 && store.read_only)">
<draggable
v-show="has_tabs"
class="tabs"
v-model="layout.tabs"
v-model="store.form.layout.tabs"
group="tabs"
filter="[data-has-std-field='true']"
:prevent-on-filter="false"
@ -124,7 +123,7 @@ function delete_tab(with_children) {
>
<template #item="{ element }">
<div
:class="['tab', store.active_tab == element.df.name ? 'active' : '']"
:class="['tab', store.form.active_tab == element.df.name ? 'active' : '']"
:title="element.df.fieldname"
:data-is-custom="element.df.is_custom_field"
:data-has-std-field="store.has_standard_field(element)"
@ -167,9 +166,9 @@ function delete_tab(with_children) {
<div class="tab-contents">
<div
class="tab-content"
v-for="(tab, i) in layout.tabs"
v-for="(tab, i) in store.form.layout.tabs"
:key="i"
:class="[store.active_tab == tab.df.name ? 'active' : '']"
:class="[store.form.active_tab == tab.df.name ? 'active' : '']"
>
<draggable
class="tab-content-container"

View file

@ -24,7 +24,7 @@ let doctype_df = computed(() => {
doctypes.value = store
.get_updated_fields()
.filter(df => df.fieldtype == "Link")
.filter(df => df.options && df.fieldname != store.selected_field.fieldname)
.filter(df => df.options && df.fieldname != store.form.selected_field.fieldname)
.sort((a, b) => a.options.localeCompare(b.options))
.map(df => ({
label: `${df.options} (${df.fieldname})`,

View file

@ -1,270 +1,311 @@
import { defineStore } from "pinia";
import { create_layout, scrub_field_names } from "./utils";
import { nextTick } from "vue";
import { computed, nextTick, ref } from "vue";
import { useDebouncedRefHistory, onKeyDown } from "@vueuse/core";
export const useStore = defineStore("form-builder-store", {
state: () => ({
doctype: "",
doc: null,
docfields: [],
custom_docfields: [],
export const useStore = defineStore("form-builder-store", () => {
let doctype = ref("");
let doc = ref(null);
let docfields = ref([]);
let custom_docfields = ref([]);
let form = ref({
layout: {},
active_tab: "",
active_tab: null,
selected_field: null,
dirty: false,
read_only: false,
is_customize_form: false,
preview: false,
drag: false,
}),
getters: {
get_animation: () => {
return "cubic-bezier(0.34, 1.56, 0.64, 1)";
},
selected: (state) => {
return (name) => state.selected_field?.name == name;
},
get_docfields: (state) => {
return state.is_customize_form ? state.custom_docfields : state.docfields;
},
get_df: (state) => {
return (fieldtype, fieldname = "", label = "") => {
let docfield = state.is_customize_form ? "Customize Form Field" : "DocField";
let df = frappe.model.get_new_doc(docfield);
df.name = frappe.utils.get_random(8);
df.fieldtype = fieldtype;
df.fieldname = fieldname;
df.label = label;
state.is_customize_form && (df.is_custom_field = 1);
return df;
};
},
has_standard_field: (state) => {
return (field) => {
if (!state.is_customize_form) return;
if (!field.df.is_custom_field) return true;
});
let dirty = ref(false);
let read_only = ref(false);
let is_customize_form = ref(false);
let preview = ref(false);
let drag = ref(false);
let get_animation = "cubic-bezier(0.34, 1.56, 0.64, 1)";
let ref_history = ref(null);
let children = {
"Tab Break": "sections",
"Section Break": "columns",
"Column Break": "fields",
}[field.df.fieldtype];
// Getters
let get_docfields = computed(() => {
return is_customize_form.value ? custom_docfields.value : docfields.value;
});
if (!children) return false;
let current_tab = computed(() => {
return form.value.layout.tabs.find((tab) => tab.df.name == form.value.active_tab);
});
return field[children].some((child) => {
if (!child.df.is_custom_field) return true;
return state.has_standard_field(child);
});
};
},
current_tab: (state) => {
return state.layout.tabs.find((tab) => tab.df.name == state.active_tab);
},
},
actions: {
async fetch() {
await frappe.model.clear_doc("DocType", this.doctype);
await frappe.model.with_doctype(this.doctype);
// Actions
function selected(name) {
return form.value.selected_field?.name == name;
}
if (this.is_customize_form) {
await frappe.model.with_doc("Customize Form");
let doc = frappe.get_doc("Customize Form");
doc.doc_type = this.doctype;
let r = await frappe.call({ method: "fetch_to_customize", doc });
this.doc = r.docs[0];
function get_df(fieldtype, fieldname = "", label = "") {
let docfield = is_customize_form.value ? "Customize Form Field" : "DocField";
let df = frappe.model.get_new_doc(docfield);
df.name = frappe.utils.get_random(8);
df.fieldtype = fieldtype;
df.fieldname = fieldname;
df.label = label;
is_customize_form.value && (df.is_custom_field = 1);
return df;
}
function has_standard_field(field) {
if (!is_customize_form.value) return;
if (!field.df.is_custom_field) return true;
let children = {
"Tab Break": "sections",
"Section Break": "columns",
"Column Break": "fields",
}[field.df.fieldtype];
if (!children) return false;
return field[children].some((child) => {
if (!child.df.is_custom_field) return true;
return has_standard_field(child);
});
}
async function fetch() {
await frappe.model.clear_doc("DocType", doctype.value);
await frappe.model.with_doctype(doctype.value);
if (is_customize_form.value) {
await frappe.model.with_doc("Customize Form");
let _doc = frappe.get_doc("Customize Form");
_doc.doc_type = doctype.value;
let r = await frappe.call({ method: "fetch_to_customize", doc: _doc });
doc.value = r.docs[0];
} else {
doc.value = await frappe.db.get_doc("DocType", doctype.value);
}
if (!get_docfields.value.length) {
let docfield = is_customize_form.value ? "Customize Form Field" : "DocField";
await frappe.model.with_doctype(docfield);
let df = frappe.get_meta(docfield).fields;
if (is_customize_form.value) {
custom_docfields.value = df;
} else {
this.doc = await frappe.db.get_doc("DocType", this.doctype);
docfields.value = df;
}
}
form.value.layout = get_layout();
form.value.active_tab = form.value.layout.tabs[0].df.name;
form.value.selected_field = null;
nextTick(() => {
dirty.value = false;
read_only.value =
!is_customize_form.value && !frappe.boot.developer_mode && !doc.value.custom;
preview.value = false;
});
setup_undo_redo();
}
let undo_redo_keyboard_event = onKeyDown(true, (e) => {
if (e.ctrlKey || e.metaKey) {
if (e.key === "z" && !e.shiftKey && ref_history.value.canUndo) {
ref_history.value.undo();
} else if (e.key === "z" && e.shiftKey && ref_history.value.canRedo) {
ref_history.value.redo();
}
}
});
function setup_undo_redo() {
ref_history.value = useDebouncedRefHistory(form, { deep: true, debounce: 100 });
undo_redo_keyboard_event;
}
function reset_changes() {
fetch();
}
function validate_fields(fields, is_table) {
fields = scrub_field_names(fields);
let not_allowed_in_list_view = ["Attach Image", ...frappe.model.no_value_type];
if (is_table) {
not_allowed_in_list_view = not_allowed_in_list_view.filter((f) => f != "Button");
}
function get_field_data(df) {
let fieldname = `<b>${df.label} (${df.fieldname})</b>`;
if (!df.label) {
fieldname = `<b>${df.fieldname}</b>`;
}
let fieldtype = `<b>${df.fieldtype}</b>`;
return [fieldname, fieldtype];
}
fields.forEach((df) => {
// check if fieldname already exist
let duplicate = fields.filter((f) => f.fieldname == df.fieldname);
if (duplicate.length > 1) {
frappe.throw(__("Fieldname {0} appears multiple times", get_field_data(df)));
}
if (!this.get_docfields.length) {
let docfield = this.is_customize_form ? "Customize Form Field" : "DocField";
await frappe.model.with_doctype(docfield);
let df = frappe.get_meta(docfield).fields;
if (this.is_customize_form) {
this.custom_docfields = df;
} else {
this.docfields = df;
}
// Link & Table fields should always have options set
if (in_list(["Link", ...frappe.model.table_fields], df.fieldtype) && !df.options) {
frappe.throw(
__("Options is required for field {0} of type {1}", get_field_data(df))
);
}
this.layout = this.get_layout();
this.active_tab = this.layout.tabs[0].df.name;
this.selected_field = null;
nextTick(() => {
this.dirty = false;
this.read_only =
!this.is_customize_form && !frappe.boot.developer_mode && !this.doc.custom;
this.preview = false;
});
},
reset_changes() {
this.fetch();
},
validate_fields(fields, is_table) {
fields = scrub_field_names(fields);
let not_allowed_in_list_view = ["Attach Image", ...frappe.model.no_value_type];
if (is_table) {
not_allowed_in_list_view = not_allowed_in_list_view.filter((f) => f != "Button");
// Do not allow if field is hidden & required but doesn't have default value
if (df.hidden && df.reqd && !df.default) {
frappe.throw(
__(
"{0} cannot be hidden and mandatory without any default value",
get_field_data(df)
)
);
}
function get_field_data(df) {
let fieldname = `<b>${df.label} (${df.fieldname})</b>`;
if (!df.label) {
fieldname = `<b>${df.fieldname}</b>`;
}
let fieldtype = `<b>${df.fieldtype}</b>`;
return [fieldname, fieldtype];
// In List View is not allowed for some fieldtypes
if (df.in_list_view && in_list(not_allowed_in_list_view, df.fieldtype)) {
frappe.throw(
__(
"'In List View' is not allowed for field {0} of type {1}",
get_field_data(df)
)
);
}
fields.forEach((df) => {
// check if fieldname already exist
let duplicate = fields.filter((f) => f.fieldname == df.fieldname);
if (duplicate.length > 1) {
frappe.throw(__("Fieldname {0} appears multiple times", get_field_data(df)));
}
// In Global Search is not allowed for no_value_type fields
if (df.in_global_search && in_list(frappe.model.no_value_type, df.fieldtype)) {
frappe.throw(
__(
"'In Global Search' is not allowed for field {0} of type {1}",
get_field_data(df)
)
);
}
});
}
// Link & Table fields should always have options set
if (in_list(["Link", ...frappe.model.table_fields], df.fieldtype) && !df.options) {
frappe.throw(
__("Options is required for field {0} of type {1}", get_field_data(df))
);
}
async function save_changes() {
if (!dirty.value) {
frappe.show_alert({ message: __("No changes to save"), indicator: "orange" });
return;
}
// Do not allow if field is hidden & required but doesn't have default value
if (df.hidden && df.reqd && !df.default) {
frappe.throw(
__(
"{0} cannot be hidden and mandatory without any default value",
get_field_data(df)
)
);
}
frappe.dom.freeze(__("Saving..."));
// In List View is not allowed for some fieldtypes
if (df.in_list_view && in_list(not_allowed_in_list_view, df.fieldtype)) {
frappe.throw(
__(
"'In List View' is not allowed for field {0} of type {1}",
get_field_data(df)
)
);
}
try {
if (is_customize_form.value) {
let _doc = frappe.get_doc("Customize Form");
_doc.doc_type = doctype.value;
_doc.fields = get_updated_fields();
validate_fields(_doc.fields, _doc.istable);
await frappe.call({ method: "save_customization", doc: _doc });
} else {
doc.value.fields = get_updated_fields();
validate_fields(doc.value.fields, doc.value.istable);
await frappe.call("frappe.client.save", { doc: doc.value });
frappe.toast("Fields Table Updated");
}
fetch();
} catch (e) {
console.error(e);
} finally {
frappe.dom.unfreeze();
}
}
// In Global Search is not allowed for no_value_type fields
if (df.in_global_search && in_list(frappe.model.no_value_type, df.fieldtype)) {
frappe.throw(
__(
"'In Global Search' is not allowed for field {0} of type {1}",
get_field_data(df)
)
);
}
});
},
async save_changes() {
if (!this.dirty) {
frappe.show_alert({ message: __("No changes to save"), indicator: "orange" });
return;
function get_updated_fields() {
let fields = [];
let idx = 0;
let layout_fields = JSON.parse(JSON.stringify(form.value.layout.tabs));
layout_fields.forEach((tab, i) => {
if (
(i == 0 && is_df_updated(tab.df, get_df("Tab Break", "", __("Details")))) ||
i > 0
) {
idx++;
tab.df.idx = idx;
fields.push(tab.df);
}
frappe.dom.freeze(__("Saving..."));
tab.sections.forEach((section, j) => {
// data before section is added
let fields_copy = JSON.parse(JSON.stringify(fields));
let old_idx = idx;
section.has_fields = false;
try {
if (this.is_customize_form) {
let doc = frappe.get_doc("Customize Form");
doc.doc_type = this.doctype;
doc.fields = this.get_updated_fields();
this.validate_fields(doc.fields, doc.istable);
await frappe.call({ method: "save_customization", doc });
} else {
this.doc.fields = this.get_updated_fields();
this.validate_fields(this.doc.fields, this.doc.istable);
await frappe.call({
method: "frappe.desk.form.save.savedocs",
args: { doc: this.doc, action: "Save" },
});
}
this.fetch();
} catch (e) {
console.error(e);
} finally {
frappe.dom.unfreeze();
}
},
get_updated_fields() {
let fields = [];
let idx = 0;
let layout_fields = JSON.parse(JSON.stringify(this.layout.tabs));
layout_fields.forEach((tab, i) => {
if (
(i == 0 &&
this.is_df_updated(tab.df, this.get_df("Tab Break", "", __("Details")))) ||
i > 0
) {
// do not consider first section if label is not set
if ((j == 0 && is_df_updated(section.df, get_df("Section Break"))) || j > 0) {
idx++;
tab.df.idx = idx;
fields.push(tab.df);
section.df.idx = idx;
fields.push(section.df);
}
tab.sections.forEach((section, j) => {
// data before section is added
let fields_copy = JSON.parse(JSON.stringify(fields));
let old_idx = idx;
section.has_fields = false;
// do not consider first section if label is not set
section.columns.forEach((column, k) => {
// do not consider first column if label is not set
if (
(j == 0 && this.is_df_updated(section.df, this.get_df("Section Break"))) ||
j > 0
(k == 0 && is_df_updated(column.df, get_df("Column Break"))) ||
k > 0 ||
column.fields.length == 0
) {
idx++;
section.df.idx = idx;
fields.push(section.df);
column.df.idx = idx;
fields.push(column.df);
}
section.columns.forEach((column, k) => {
// do not consider first column if label is not set
if (
(k == 0 &&
this.is_df_updated(column.df, this.get_df("Column Break"))) ||
k > 0 ||
column.fields.length == 0
) {
idx++;
column.df.idx = idx;
fields.push(column.df);
}
column.fields.forEach((field) => {
idx++;
field.df.idx = idx;
fields.push(field.df);
section.has_fields = true;
});
column.fields.forEach((field) => {
idx++;
field.df.idx = idx;
fields.push(field.df);
section.has_fields = true;
});
// restore data back to data before section is added.
if (!section.has_fields) {
fields = fields_copy || [];
idx = old_idx;
}
});
});
return fields;
},
is_df_updated(df, new_df) {
delete df.name;
delete new_df.name;
return JSON.stringify(df) != JSON.stringify(new_df);
},
get_layout() {
return create_layout(this.doc.fields);
},
},
// restore data back to data before section is added.
if (!section.has_fields) {
fields = fields_copy || [];
idx = old_idx;
}
});
});
return fields;
}
function is_df_updated(df, new_df) {
delete df.name;
delete new_df.name;
return JSON.stringify(df) != JSON.stringify(new_df);
}
function get_layout() {
return create_layout(doc.value.fields);
}
return {
doctype,
doc,
form,
dirty,
read_only,
is_customize_form,
preview,
drag,
get_animation,
get_docfields,
current_tab,
selected,
get_df,
has_standard_field,
fetch,
reset_changes,
validate_fields,
save_changes,
get_updated_fields,
is_df_updated,
get_layout,
};
});

View file

@ -34,15 +34,6 @@ frappe.Application = class Application {
frappe.socketio.init();
frappe.model.init();
if (frappe.boot.status === "failed") {
frappe.msgprint({
message: frappe.boot.error,
title: __("Session Start Failed"),
indicator: "red",
});
throw "boot failed";
}
this.load_bootinfo();
this.load_user_permissions();
this.make_nav_bar();

View file

@ -406,7 +406,10 @@ frappe.ui.form.Form = class FrappeForm {
// read only (workflow)
this.read_only = frappe.workflow.is_read_only(this.doctype, this.docname);
if (this.read_only) this.set_read_only(true);
if (this.read_only) {
this.set_read_only(true);
frappe.show_alert(__("This form is not editable due to a Workflow."));
}
// check if doctype is already open
if (!this.opendocs[this.docname]) {

View file

@ -477,11 +477,16 @@ export default class Grid {
this.wrapper.find(".grid-add-multiple-rows").removeClass("hidden");
}
}
} else if (this.grid_rows.length < this.grid_pagination.page_length) {
} else if (
this.grid_rows.length < this.grid_pagination.page_length &&
!this.df.allow_bulk_edit
) {
this.wrapper.find(".grid-footer").toggle(false);
}
this.wrapper.find(".grid-add-row, .grid-add-multiple-rows").toggle(this.is_editable());
this.wrapper
.find(".grid-add-row, .grid-add-multiple-rows, .grid-upload")
.toggle(this.is_editable());
}
truncate_rows() {

View file

@ -0,0 +1,98 @@
export class ReminderManager {
constructor({ frm }) {
this.frm = frm; // can be optional if not setting for document.
}
show() {
let me = this;
this.dialog = new frappe.ui.Dialog({
title: __("Create a Reminder"),
fields: [
{
fieldtype: "Select",
label: __("Remind Me In"),
fieldname: "remind_me_in",
options: [
{ label: __("30 minutes"), value: "30_minutes" },
{ label: __("1 hour"), value: "1_hour" },
{ label: __("4 hours"), value: "4_hours" },
{ label: __("1 Day"), value: "1_day" },
{ label: __("Custom"), value: "custom" },
],
default: "1_hour",
onchange: () => {
me._update_datetime_selector();
},
},
{
fieldtype: "Column Break",
fieldname: "col_break_1",
},
{
fieldtype: "Datetime",
label: __("Remind At"),
fieldname: "remind_at",
reqd: 1,
read_only: 1,
},
{
fieldtype: "Section Break",
fieldname: "divider_between_message_and_time",
},
{
fieldtype: "Small Text",
label: __("Description"),
fieldname: "description",
reqd: 1,
},
],
primary_action_label: __("Create"),
primary_action: () => {
this.create_reminder();
this.dialog.hide();
},
secondary_action_label: __("Cancel"),
secondary_action: () => {
this.dialog.hide();
},
});
this._update_datetime_selector();
this.dialog.show();
}
_update_datetime_selector() {
this._convert_period_to_absolute_time();
this.dialog.fields_dict.remind_at.df.read_only =
this.dialog.get_value("remind_me_in") != "custom";
this.dialog.fields_dict.remind_at.refresh();
this.dialog.fields_dict.remind_at.datepicker?.update({
minDate: frappe.datetime.str_to_obj(frappe.datetime.now_datetime()),
});
}
_convert_period_to_absolute_time() {
const period = this.dialog.get_value("remind_me_in");
if (!period || period == "custom") return;
const now_time = frappe.datetime.str_to_obj(frappe.datetime.now_datetime());
let [magnitude, unit] = period.split("_");
let time_to_set = moment(now_time)
.add(magnitude, unit)
.format(frappe.defaultDatetimeFormat);
this.dialog.set_value("remind_at", time_to_set);
}
create_reminder() {
frappe
.xcall("frappe.automation.doctype.reminder.reminder.create_new_reminder", {
remind_at: this.dialog.get_value("remind_at"),
description: this.dialog.get_value("description"),
reminder_doctype: this.frm?.doc.doctype,
reminder_docname: this.frm?.doc.name,
})
.then((reminder) => {
frappe.show_alert(__("Reminder set at {0}", [reminder.remind_at]));
});
}
}

View file

@ -2,6 +2,7 @@
// MIT License. See license.txt
import "./linked_with";
import "./form_viewers";
import { ReminderManager } from "./reminders";
frappe.ui.form.Toolbar = class Toolbar {
constructor(opts) {
@ -436,6 +437,19 @@ frappe.ui.form.Toolbar = class Toolbar {
);
}
this.page.add_menu_item(
__("Remind Me"),
() => {
let reminder_maanger = new ReminderManager({ frm: this.frm });
reminder_maanger.show();
},
true,
{
shortcut: "Shift+R",
condition: () => !this.frm.is_new(),
}
);
this.make_customize_buttons();
// Auto Repeat

View file

@ -102,6 +102,7 @@ frappe.ui.form.States = class FormStates {
added = true;
me.frm.page.add_action_item(__(d.action), function () {
// set the workflow_action for use in form scripts
frappe.dom.freeze();
me.frm.selected_workflow_action = d.action;
me.frm.script_manager.trigger("before_workflow_action").then(() => {
frappe
@ -111,6 +112,7 @@ frappe.ui.form.States = class FormStates {
})
.then((doc) => {
frappe.model.sync(doc);
frappe.dom.unfreeze();
me.frm.refresh();
me.frm.selected_workflow_action = null;
me.frm.script_manager.trigger("after_workflow_action");

View file

@ -51,14 +51,14 @@ class Picker {
set_values();
}
});
this.search_input.keyup((e) => {
e.preventDefault();
this.filter_icons();
});
});
this.search_input.keyup((e) => {
e.preventDefault();
this.filter_icons();
});
this.search_input.on("search", () => {
this.filter_icons();
});
this.search_input.on("search", () => {
this.filter_icons();
});
}

View file

@ -304,7 +304,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}
refresh(refresh_header = false) {
super.refresh().then(() => {
return super.refresh().then(() => {
this.render_header(refresh_header);
this.update_checkbox();
this.update_url_with_filters();
@ -504,9 +504,13 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
get_args() {
const args = super.get_args();
return Object.assign(args, {
with_comment_count: true,
});
if (this.list_view_settings && !this.list_view_settings.disable_comment_count) {
args.with_comment_count = 1;
} else {
args.with_comment_count = 0;
}
return args;
}
before_refresh() {
@ -896,10 +900,13 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
</div>`;
}
const comment_count = `<span class="comment-count">
let comment_count = null;
if (this.list_view_settings && !this.list_view_settings.disable_comment_count) {
comment_count = $(`<span class="comment-count"></span>`);
$(comment_count).append(`
${frappe.utils.icon("small-message")}
${doc._comment_count > 99 ? "99+" : doc._comment_count || 0}
</span>`;
${doc._comment_count > 99 ? "99+" : doc._comment_count || 0}`);
}
html += `
<div class="level-item list-row-activity hidden-xs">
@ -907,7 +914,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
${settings_button || assigned_to}
</div>
${modified}
${comment_count}
${comment_count ? $(comment_count).prop("outerHTML") : ""}
</div>
<div class="level-item visible-xs text-right">
${this.get_indicator_dot(doc)}
@ -1526,7 +1533,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
});
}
if (frappe.model.can_set_user_permissions(doctype)) {
if (frappe.user_roles.includes("System Manager")) {
items.push({
label: __("User Permissions", null, "Button in list view menu"),
action: () =>

View file

@ -149,9 +149,13 @@ $.extend(frappe.model, {
cur_frm.doc.doctype === doc.doctype &&
cur_frm.doc.name === doc.name
) {
if (!frappe.ui.form.is_saving && data.modified != cur_frm.doc.modified) {
doc.__needs_refresh = true;
cur_frm.show_conflict_message();
if (data.modified !== cur_frm.doc.modified) {
if (!cur_frm.is_dirty()) {
cur_frm.reload_doc();
} else if (!frappe.ui.form.is_saving) {
doc.__needs_refresh = true;
cur_frm.show_conflict_message();
}
}
} else {
if (!doc.__unsaved) {
@ -449,14 +453,6 @@ $.extend(frappe.model, {
return frappe.boot.user.can_share.indexOf(doctype) !== -1;
},
can_set_user_permissions: function (doctype, frm) {
// system manager can always set user permissions
if (frappe.user_roles.includes("System Manager")) return true;
if (frm) return frm.perm[0].set_user_permissions === 1;
return frappe.boot.user.can_set_user_permissions.indexOf(doctype) !== -1;
},
has_value: function (dt, dn, fn) {
// return true if property has value
var val = locals[dt] && locals[dt][dn] && locals[dt][dn][fn];

View file

@ -30,7 +30,6 @@ $.extend(frappe.perm, {
"print",
"email",
"share",
"set_user_permissions",
],
doctype_perm: {},

View file

@ -47,6 +47,8 @@ frappe.ui.Filter = class {
Color: ["Between", "Timespan"],
Check: this.conditions.map((c) => c[0]).filter((c) => c !== "="),
Code: ["Between", "Timespan", ">", "<", ">=", "<=", "in", "not in"],
"HTML Editor": ["Between", "Timespan", ">", "<", ">=", "<=", "in", "not in"],
"Markdown Editor": ["Between", "Timespan", ">", "<", ">=", "<=", "in", "not in"],
Password: ["Between", "Timespan", ">", "<", ">=", "<=", "in", "not in"],
Rating: ["like", "not like", "Between", "in", "not in", "Timespan"],
};

View file

@ -237,18 +237,18 @@ class NotificationsView extends BaseNotificationsView {
this.change_activity_status();
}
get_dropdown_item_html(field) {
let doc_link = this.get_item_link(field);
get_dropdown_item_html(notification_log) {
let doc_link = this.get_item_link(notification_log);
let read_class = field.read ? "" : "unread";
let message = field.subject;
let read_class = notification_log.read ? "" : "unread";
let message = notification_log.subject;
let title = message.match(/<b class="subject-title">(.*?)<\/b>/);
message = title
? message.replace(title[1], frappe.ellipsis(strip_html(title[1]), 100))
: message;
let timestamp = frappe.datetime.comment_when(field.creation);
let timestamp = frappe.datetime.comment_when(notification_log.creation);
let message_html = `<div class="message">
<div>${message}</div>
<div class="notification-timestamp text-muted">
@ -256,12 +256,12 @@ class NotificationsView extends BaseNotificationsView {
</div>
</div>`;
let user = field.from_user;
let user = notification_log.from_user;
let user_avatar = frappe.avatar(user, "avatar-medium user-avatar");
let item_html = $(`<a class="recent-item notification-item ${read_class}"
href="${doc_link}"
data-name="${field.name}"
data-name="${notification_log.name}"
>
<div class="notification-body">
${user_avatar}
@ -271,18 +271,18 @@ class NotificationsView extends BaseNotificationsView {
</div>
</a>`);
if (!field.read) {
if (!notification_log.read) {
let mark_btn = item_html.find(".mark-as-read");
mark_btn.tooltip({ delay: { show: 600, hide: 100 }, trigger: "hover" });
mark_btn.on("click", (e) => {
e.preventDefault();
e.stopImmediatePropagation();
this.mark_as_read(field.name, item_html);
this.mark_as_read(notification_log.name, item_html);
});
}
item_html.on("click", () => {
!field.read && this.mark_as_read(field.name, item_html);
!notification_log.read && this.mark_as_read(notification_log.name, item_html);
this.notifications_icon.trigger("click");
});
@ -298,8 +298,8 @@ class NotificationsView extends BaseNotificationsView {
} else {
if (this.dropdown_items.length) {
this.container.empty();
this.dropdown_items.forEach((field) => {
this.container.append(this.get_dropdown_item_html(field));
this.dropdown_items.forEach((notification_log) => {
this.container.append(this.get_dropdown_item_html(notification_log));
});
this.container.append(`<a class="list-footer"
href="/app/List/Notification Log">

View file

@ -7,7 +7,7 @@
<div class="collapse navbar-collapse justify-content-end">
<form class="form-inline fill-width justify-content-end" role="search" onsubmit="return false;">
{% if (frappe.boot.read_only) { %}
<span class="indicator-pill yellow no-indicator-dot" title="{%= __("Your site is getting upgraded.") %}">
<span class="indicator-pill yellow no-indicator-dot" title="{%= __("Your site is undergoing maintenance or being updated.") %}">
{%= __("Read Only Mode") %}
</span>
{% } %}

View file

@ -120,6 +120,7 @@ frappe.views.Calendar = class Calendar {
start: "start",
end: "end",
allDay: "all_day",
convertToUserTz: "convert_to_user_tz",
};
this.color_map = {
danger: "red",
@ -372,10 +373,13 @@ frappe.views.Calendar = class Calendar {
});
if (!me.field_map.allDay) d.allDay = 1;
if (!me.field_map.convertToUserTz) d.convertToUserTz = 1;
// convert to user tz
d.start = frappe.datetime.convert_to_user_tz(d.start);
d.end = frappe.datetime.convert_to_user_tz(d.end);
if (d.convertToUserTz) {
d.start = frappe.datetime.convert_to_user_tz(d.start);
d.end = frappe.datetime.convert_to_user_tz(d.end);
}
// show event on single day if start or end date is invalid
if (!frappe.datetime.validate(d.start) && d.end) {

View file

@ -36,10 +36,9 @@
</td>
{% for col in columns %}
{% if col.name && col._id !== "_check" %}
{% var value = col.fieldname ? row[col.fieldname] : row[col.id]; %}
<td {% if row.bold == 1 %} style="font-weight: bold" {% endif %}>
{% var value = col.fieldname ? row[col.fieldname] : row[col.id] %}
{% var longest_word = cstr(value).split(' ').reduce((longest, word) => word.length > longest.length ? word : longest, ''); %}
<td {% if row.bold == 1 %} style="font-weight: bold" {% endif %} {% if longest_word.length > 45 %} class="overflow-wrap-anywhere" {% endif %}>
<span {% if col._index == 0 %} style="padding-left: {%= cint(row.indent) * 2 %}em" {% endif %}>
{% format_data = row.is_total_row && ["Currency", "Float"].includes(col.fieldtype) ? data[0] : row %}
{% if (row.is_total_row && col._index == 0) { %}

View file

@ -1681,7 +1681,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
doctype: "Report",
name: this.report_name,
}),
condition: () => frappe.model.can_set_user_permissions("Report"),
condition: () => frappe.user.has_role("System Manager"),
standard: true,
},
];

View file

@ -1581,7 +1581,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
}
// user permissions
if (this.report_name && frappe.model.can_set_user_permissions("Report")) {
if (this.report_name && frappe.user.has_role("System Manager")) {
items.push({
label: __("User Permissions"),
action: () => {

View file

@ -34,4 +34,10 @@
img {
margin: auto;
}
}
.overflow-wrap-anywhere {
* {
overflow-wrap: anywhere;
}
}

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

@ -845,11 +845,14 @@ class TestDBQuery(FrappeTestCase):
self.assertTrue("count" in data[0])
self.assertEqual(len(data[0]), 2)
with self.assertRaises(frappe.PermissionError):
frappe.get_list(
"Blog Post",
fields=["blog_category.description"],
)
data = frappe.get_list(
"Blog Post",
fields=["name", "blogger.full_name as blogger_full_name", "blog_category.description"],
limit=1,
)
self.assertTrue("name" in data[0])
self.assertTrue("blogger_full_name" in data[0])
self.assertTrue("description" in data[0])
def test_cast_name(self):
from frappe.core.doctype.doctype.test_doctype import new_doctype

View file

@ -4,6 +4,7 @@ import json
import frappe
from frappe.desk.listview import get_group_by_count, get_list_settings, set_list_settings
from frappe.desk.reportview import get
from frappe.tests.utils import FrappeTestCase
@ -22,6 +23,7 @@ class TestListView(FrappeTestCase):
self.assertEqual(settings.disable_auto_refresh, 0)
self.assertEqual(settings.disable_count, 0)
self.assertEqual(settings.disable_comment_count, 0)
self.assertEqual(settings.disable_sidebar_stats, 0)
def test_get_list_settings_with_non_default_settings(self):
@ -31,6 +33,7 @@ class TestListView(FrappeTestCase):
self.assertEqual(settings.disable_auto_refresh, 0)
self.assertEqual(settings.disable_count, 1)
self.assertEqual(settings.disable_comment_count, 0)
self.assertEqual(settings.disable_sidebar_stats, 0)
def test_set_list_settings_without_settings(self):
@ -39,6 +42,7 @@ class TestListView(FrappeTestCase):
self.assertEqual(settings.disable_auto_refresh, 0)
self.assertEqual(settings.disable_count, 0)
self.assertEqual(settings.disable_comment_count, 0)
self.assertEqual(settings.disable_sidebar_stats, 0)
def test_set_list_settings_with_existing_settings(self):
@ -48,6 +52,7 @@ class TestListView(FrappeTestCase):
self.assertEqual(settings.disable_auto_refresh, 1)
self.assertEqual(settings.disable_count, 0)
self.assertEqual(settings.disable_comment_count, 0)
self.assertEqual(settings.disable_sidebar_stats, 0)
def test_list_view_child_table_filter_with_created_by_filter(self):
@ -65,3 +70,18 @@ class TestListView(FrappeTestCase):
for d in get_group_by_count("Note", '[["Note Seen By","user","=","Administrator"]]', "owner")
}
self.assertEqual(data["Administrator"], 1)
def test_list_view_comment_count(self):
frappe.form_dict.doctype = "DocType"
frappe.form_dict.limit = "1"
frappe.form_dict.fields = [
"`tabDocType`.`name`",
]
for with_comment_count in (1, True, "1"):
frappe.form_dict.with_comment_count = with_comment_count
self.assertEqual(len(get()["values"][0]), 2)
for with_comment_count in (0, False, "0", None):
frappe.form_dict.with_comment_count = with_comment_count
self.assertEqual(len(get()["values"][0]), 1)

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

@ -40,7 +40,6 @@ class UserPermissions:
self.can_export = []
self.can_print = []
self.can_email = []
self.can_set_user_permissions = []
self.allow_modules = []
self.in_create = []
self.setup_user()
@ -152,7 +151,7 @@ class UserPermissions:
if p.get("read") or p.get("write") or p.get("create"):
if p.get("report"):
self.can_get_report.append(dt)
for key in ("import", "export", "print", "email", "set_user_permissions"):
for key in ("import", "export", "print", "email"):
if p.get(key):
getattr(self, "can_" + key).append(dt)
@ -248,7 +247,6 @@ class UserPermissions:
"can_import",
"can_print",
"can_email",
"can_set_user_permissions",
):
d[key] = list(set(getattr(self, key)))

View file

@ -81,7 +81,6 @@
"read": 1,
"report": 1,
"role": "Website Manager",
"set_user_permissions": 1,
"share": 1,
"write": 1
},
@ -98,4 +97,4 @@
"states": [],
"title_field": "title",
"track_changes": 1
}
}

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",
@ -229,7 +230,6 @@
"read": 1,
"report": 1,
"role": "Website Manager",
"set_user_permissions": 1,
"share": 1,
"write": 1
},
@ -250,4 +250,4 @@
"states": [],
"title_field": "title",
"track_changes": 1
}
}

View file

@ -82,7 +82,6 @@
"read": 1,
"report": 1,
"role": "Website Manager",
"set_user_permissions": 1,
"share": 1,
"write": 1
},
@ -100,4 +99,4 @@
"states": [],
"title_field": "full_name",
"track_changes": 1
}
}

View file

@ -31,7 +31,7 @@ class StaticPage(BaseRenderer):
return self.is_valid_file_path() and self.file_path
def is_valid_file_path(self):
extension = self.path.rsplit(".", 1)[-1]
extension = self.path.rsplit(".", 1)[-1] if "." in self.path else ""
if extension in UNSUPPORTED_STATIC_PAGE_TYPES:
return False
return True

View file

@ -444,7 +444,9 @@ def filter_allowed_users(users, doc, transition):
filtered_users = []
for user in users:
if has_approval_access(user, doc, transition) and has_permission(doctype=doc, user=user):
if has_approval_access(user, doc, transition) and has_permission(
doctype=doc, user=user, raise_exception=False
):
filtered_users.append(user)
return filtered_users

View file

@ -27,8 +27,7 @@ def get_context(context):
try:
boot = frappe.sessions.get()
except Exception as e:
boot = frappe._dict(status="failed", error=str(e))
print(frappe.get_traceback())
raise frappe.SessionBootFailed from e
# this needs commit
csrf_token = frappe.sessions.get_csrf_token()