Merge branch 'develop' into fix-note-2
This commit is contained in:
commit
b84ab9de87
77 changed files with 1055 additions and 582 deletions
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
0
frappe/automation/doctype/reminder/__init__.py
Normal file
0
frappe/automation/doctype/reminder/__init__.py
Normal file
8
frappe/automation/doctype/reminder/reminder.js
Normal file
8
frappe/automation/doctype/reminder/reminder.js
Normal 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) {
|
||||
|
||||
// },
|
||||
// });
|
||||
90
frappe/automation/doctype/reminder/reminder.json
Normal file
90
frappe/automation/doctype/reminder/reminder.json
Normal 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"
|
||||
}
|
||||
78
frappe/automation/doctype/reminder/reminder.py
Normal file
78
frappe/automation/doctype/reminder/reminder.py
Normal 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()
|
||||
28
frappe/automation/doctype/reminder/test_reminder.py
Normal file
28
frappe/automation/doctype/reminder/test_reminder.py
Normal 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}",
|
||||
)
|
||||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -204,7 +204,6 @@
|
|||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -275,7 +275,6 @@
|
|||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -4,5 +4,6 @@
|
|||
# import frappe
|
||||
{base_class_import}
|
||||
|
||||
|
||||
class {classname}({base_class}):
|
||||
{custom_controller}
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,8 @@ DEFAULT_LOGTYPES_RETENTION = {
|
|||
"Route History": 90,
|
||||
"Submission Queue": 30,
|
||||
"Prepared Report": 30,
|
||||
"Webhook Request Log": 30,
|
||||
"Reminder": 30,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -320,7 +320,6 @@ frappe.PermissionEngine = class PermissionEngine {
|
|||
"report",
|
||||
"import",
|
||||
"export",
|
||||
"set_user_permissions",
|
||||
"share",
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@
|
|||
"report": 1,
|
||||
"export": 1,
|
||||
"import": 0,
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"print": 1,
|
||||
"email": 1,
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@
|
|||
"report": 1,
|
||||
"export": 1,
|
||||
"import": 0,
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"print": 1,
|
||||
"email": 1,
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,6 +192,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",
|
||||
|
|
@ -390,6 +391,7 @@ ignore_links_on_delete = [
|
|||
"Document Share Key",
|
||||
"Integration Request",
|
||||
"Unhandled Email",
|
||||
"Webhook Request Log",
|
||||
]
|
||||
|
||||
# Request Hooks
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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))))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 []:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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})`,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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]) {
|
||||
|
|
|
|||
98
frappe/public/js/frappe/form/reminders.js
Normal file
98
frappe/public/js/frappe/form/reminders.js
Normal 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]));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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: () =>
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ $.extend(frappe.perm, {
|
|||
"print",
|
||||
"email",
|
||||
"share",
|
||||
"set_user_permissions",
|
||||
],
|
||||
|
||||
doctype_perm: {},
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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: () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,7 +230,6 @@
|
|||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Website Manager",
|
||||
"set_user_permissions": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
|
|
@ -251,4 +250,4 @@
|
|||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue