Merge branch 'develop' into fix-note-2

This commit is contained in:
Raffael Meyer 2023-02-24 12:13:58 +01:00 committed by GitHub
commit b84ab9de87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 1055 additions and 582 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

@ -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

@ -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

@ -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

@ -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

@ -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

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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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
}
}

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()