Merge remote-tracking branch 'upstream/develop' into list-padding

This commit is contained in:
barredterra 2024-04-09 18:54:18 +02:00
commit 2c8dbb730a
73 changed files with 49917 additions and 8001 deletions

View file

@ -1,44 +0,0 @@
context("API Resources", () => {
before(() => {
cy.visit("/login");
cy.login();
cy.visit("/app/website");
});
it("Creates two Comments", () => {
cy.insert_doc("Comment", { comment_type: "Comment", content: "hello" });
cy.insert_doc("Comment", { comment_type: "Comment", content: "world" });
});
it("Lists the Comments", () => {
cy.get_list("Comment")
.its("data")
.then((data) => expect(data.length).to.be.at.least(2));
cy.get_list("Comment", ["name", "content"], [["content", "=", "hello"]]).then((body) => {
expect(body).to.have.property("data");
expect(body.data).to.have.lengthOf(1);
expect(body.data[0]).to.have.property("content");
expect(body.data[0]).to.have.property("name");
});
});
it("Gets each Comment", () => {
cy.get_list("Comment").then((body) =>
body.data.forEach((comment) => {
cy.get_doc("Comment", comment.name);
})
);
});
it("Removes the Comments", () => {
cy.get_list("Comment").then((body) => {
let comment_names = [];
body.data.map((comment) => comment_names.push(comment.name));
comment_names = [...new Set(comment_names)]; // remove duplicates
comment_names.forEach((comment_name) => {
cy.remove_doc("Comment", comment_name);
});
});
});
});

View file

@ -22,21 +22,19 @@ context("List View", () => {
const actions = [
"Approve",
"Reject",
"Edit",
"Export",
"Assign To",
"Clear Assignment",
"Apply Assignment Rule",
"Add Tags",
"Print",
"Delete",
];
cy.go_to_list("ToDo");
cy.clear_filters();
cy.get(".list-header-subject > .list-subject > .list-check-all").click();
cy.findByRole("button", { name: "Actions" }).click();
cy.get(".dropdown-menu li:visible .dropdown-item")
.should("have.length", 10)
.should("have.length", 8)
.each((el, index) => {
cy.wrap(el).contains(actions[index]);
})

View file

@ -340,6 +340,8 @@ class LoginManager:
if user == frappe.session.user:
delete_session(frappe.session.sid, user=user, reason="User Manually Logged Out")
self.clear_cookies()
if frappe.request:
self.login_as_guest()
else:
clear_sessions(user)

View file

@ -8,6 +8,7 @@ import frappe
import frappe.defaults
import frappe.desk.desk_page
from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo, get_navbar_settings
from frappe.desk.doctype.changelog_feed.changelog_feed import get_changelog_feed_items
from frappe.desk.doctype.form_tour.form_tour import get_onboarding_ui_tours
from frappe.desk.doctype.route_history.route_history import frequently_visited_links
from frappe.desk.form.load import get_meta_bundle
@ -107,6 +108,7 @@ def get_bootinfo():
bootinfo.translated_doctypes = get_translated_doctypes()
bootinfo.subscription_conf = add_subscription_conf()
bootinfo.marketplace_apps = get_marketplace_apps()
bootinfo.changelog_feed = get_changelog_feed_items()
return bootinfo

View file

@ -358,7 +358,7 @@ def get_address_display_list(doctype: str, name: str) -> list[dict]:
["Dynamic Link", "parenttype", "=", "Address"],
],
fields=["*"],
order_by="is_primary_address DESC, creation ASC",
order_by="is_primary_address DESC, `tabAddress`.creation ASC",
)
for a in address_list:
a["display"] = get_address_display(a)

View file

@ -386,7 +386,7 @@ def get_contact_display_list(doctype: str, name: str) -> list[dict]:
["Dynamic Link", "parenttype", "=", "Contact"],
],
fields=["*"],
order_by="is_primary_contact DESC, creation ASC",
order_by="is_primary_contact DESC, `tabContact`.creation ASC",
)
for contact in contact_list:

View file

@ -8,13 +8,16 @@ from frappe.tests.utils import FrappeTestCase
class TestActivityLog(FrappeTestCase):
def setUp(self) -> None:
frappe.set_user("Administrator")
def test_activity_log(self):
# test user login log
frappe.local.form_dict = frappe._dict(
{
"cmd": "login",
"sid": "Guest",
"pwd": frappe.conf.admin_password or "admin",
"pwd": self.ADMIN_PASSWORD or "admin",
"usr": "Administrator",
}
)
@ -57,7 +60,7 @@ class TestActivityLog(FrappeTestCase):
update_system_settings({"allow_consecutive_login_attempts": 3, "allow_login_after_fail": 5})
frappe.local.form_dict = frappe._dict(
{"cmd": "login", "sid": "Guest", "pwd": "admin", "usr": "Administrator"}
{"cmd": "login", "sid": "Guest", "pwd": self.ADMIN_PASSWORD, "usr": "Administrator"}
)
frappe.local.request_ip = "127.0.0.1"

View file

@ -10,6 +10,7 @@ import frappe
from frappe.cache_manager import clear_doctype_cache
from frappe.core.doctype.doctype.doctype import (
CannotIndexedError,
DocType,
DoctypeLinkError,
HiddenAndMandatoryWithoutDefaultError,
IllegalMandatoryError,
@ -782,7 +783,7 @@ def new_doctype(
custom: bool = True,
default: str | None = None,
**kwargs,
):
) -> "DocType":
if not name:
# Test prefix is required to avoid coverage
name = "Test " + "".join(random.sample(string.ascii_lowercase, 10))

View file

@ -1,7 +1,4 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on("Navbar Settings", {
// refresh: function(frm) {
// }
});
frappe.ui.form.on("Navbar Settings", {});

View file

@ -11,7 +11,9 @@
"logo_width",
"section_break_2",
"settings_dropdown",
"help_dropdown"
"help_dropdown",
"announcements_section",
"announcement_widget"
],
"fields": [
{
@ -49,11 +51,23 @@
"fieldname": "logo_width",
"fieldtype": "Int",
"label": "Logo Width"
},
{
"fieldname": "announcements_section",
"fieldtype": "Section Break",
"label": "Announcements"
},
{
"description": "These announcements will appear inside a dismissible alert below the Navbar.",
"fieldname": "announcement_widget",
"fieldtype": "Text Editor",
"label": "Announcement Widget",
"max_height": "10em"
}
],
"issingle": 1,
"links": [],
"modified": "2024-03-23 16:03:30.561647",
"modified": "2024-03-23 17:03:30.561647",
"modified_by": "Administrator",
"module": "Core",
"name": "Navbar Settings",
@ -75,4 +89,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View file

@ -16,6 +16,7 @@ class NavbarSettings(Document):
from frappe.core.doctype.navbar_item.navbar_item import NavbarItem
from frappe.types import DF
announcement_widget: DF.TextEditor | None
app_logo: DF.AttachImage | None
help_dropdown: DF.Table[NavbarItem]
logo_width: DF.Int

View file

@ -125,7 +125,7 @@ class ScheduledJobType(Document):
next_execution = croniter(self.cron_format, last_execution).get_next(datetime)
jitter = 0
if self.frequency in ("Hourly Long", "Daily Long"):
if "Long" in self.frequency:
jitter = randint(1, 600)
return next_execution + timedelta(seconds=jitter)

View file

@ -78,7 +78,7 @@ class TestScheduledJobType(FrappeTestCase):
dict(method="frappe.social.doctype.energy_point_log.energy_point_log.send_weekly_summary"),
)
job.db_set("last_execution", "2019-01-01 00:00:00")
self.assertTrue(job.is_event_due(get_datetime("2019-01-06 00:00:01")))
self.assertTrue(job.is_event_due(get_datetime("2019-01-06 00:10:01"))) # +10 min because of jitter
self.assertFalse(job.is_event_due(get_datetime("2019-01-02 00:00:06")))
self.assertFalse(job.is_event_due(get_datetime("2019-01-05 23:59:59")))

View file

@ -57,7 +57,7 @@
"fieldname": "doctype_event",
"fieldtype": "Select",
"label": "DocType Event",
"options": "Before Insert\nBefore Validate\nBefore Save\nAfter Insert\nAfter Save\nBefore Rename\nAfter Rename\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)\nOn Payment Authorization\nOn Payment Paid\nOn Payment Failed"
"options": "Before Insert\nBefore Validate\nBefore Save\nAfter Insert\nAfter Save\nBefore Rename\nAfter Rename\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)\nBefore Print\nOn Payment Authorization\nOn Payment Paid\nOn Payment Failed"
},
{
"depends_on": "eval:doc.script_type==='API'",
@ -151,7 +151,7 @@
"link_fieldname": "server_script"
}
],
"modified": "2024-03-23 16:03:38.075313",
"modified": "2024-04-08 16:18:52.901097",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",

View file

@ -46,6 +46,7 @@ class ServerScript(Document):
"After Delete",
"Before Save (Submitted Document)",
"After Save (Submitted Document)",
"Before Print",
"On Payment Authorization",
"On Payment Paid",
"On Payment Failed",

View file

@ -19,6 +19,7 @@ EVENT_MAP = {
"after_delete": "After Delete",
"before_update_after_submit": "Before Save (Submitted Document)",
"on_update_after_submit": "After Save (Submitted Document)",
"before_print": "Before Print",
"on_payment_paid": "On Payment Paid",
"on_payment_failed": "On Payment Failed",
"on_payment_authorized": "On Payment Authorization",

View file

@ -291,7 +291,7 @@ class TestUser(FrappeTestCase):
res1 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
res2 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
self.assertEqual(res1.status_code, 404)
self.assertEqual(res2.status_code, 417)
self.assertEqual(res2.status_code, 429)
def test_user_rename(self):
old_name = "test_user_rename@example.com"

View file

@ -814,7 +814,7 @@
"write": 1
},
{
"role": "All",
"role": "Desk User",
"select": 1
}
],
@ -827,4 +827,4 @@
"states": [],
"title_field": "full_name",
"track_changes": 1
}
}

View file

@ -161,11 +161,11 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
self.db_type = "mariadb"
self.type_map = {
"Currency": ("decimal", "21,9"),
"Int": ("int", "11"),
"Int": ("int", None),
"Long Int": ("bigint", "20"),
"Float": ("decimal", "21,9"),
"Percent": ("decimal", "21,9"),
"Check": ("int", "1"),
"Check": ("tinyint", None),
"Small Text": ("text", ""),
"Long Text": ("longtext", ""),
"Code": ("longtext", ""),
@ -288,7 +288,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
`name` VARCHAR(255) NOT NULL,
`fieldname` VARCHAR(140) NOT NULL,
`password` TEXT NOT NULL,
`encrypted` INT(1) NOT NULL DEFAULT 0,
`encrypted` TINYINT NOT NULL DEFAULT 0,
PRIMARY KEY (`doctype`, `name`, `fieldname`)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci"""
)
@ -303,7 +303,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
content text,
fulltext(content),
route varchar({self.VARCHAR_LEN}),
published int(1) not null default 0,
published TINYINT not null default 0,
unique `doctype_name` (doctype, name))
COLLATE=utf8mb4_unicode_ci
ENGINE=MyISAM

View file

@ -13,61 +13,61 @@ CREATE TABLE `tabDocField` (
`modified` datetime(6) DEFAULT NULL,
`modified_by` varchar(255) DEFAULT NULL,
`owner` varchar(255) DEFAULT NULL,
`docstatus` int(1) NOT NULL DEFAULT 0,
`docstatus` tinyint NOT NULL DEFAULT 0,
`parent` varchar(255) DEFAULT NULL,
`parentfield` varchar(255) DEFAULT NULL,
`parenttype` varchar(255) DEFAULT NULL,
`idx` int(8) NOT NULL DEFAULT 0,
`idx` int NOT NULL DEFAULT 0,
`fieldname` varchar(255) DEFAULT NULL,
`label` varchar(255) DEFAULT NULL,
`oldfieldname` varchar(255) DEFAULT NULL,
`fieldtype` varchar(255) DEFAULT NULL,
`oldfieldtype` varchar(255) DEFAULT NULL,
`options` text,
`search_index` int(1) NOT NULL DEFAULT 0,
`show_dashboard` int(1) NOT NULL DEFAULT 0,
`hidden` int(1) NOT NULL DEFAULT 0,
`set_only_once` int(1) NOT NULL DEFAULT 0,
`allow_in_quick_entry` int(1) NOT NULL DEFAULT 0,
`print_hide` int(1) NOT NULL DEFAULT 0,
`report_hide` int(1) NOT NULL DEFAULT 0,
`reqd` int(1) NOT NULL DEFAULT 0,
`bold` int(1) NOT NULL DEFAULT 0,
`in_global_search` int(1) NOT NULL DEFAULT 0,
`collapsible` int(1) NOT NULL DEFAULT 0,
`unique` int(1) NOT NULL DEFAULT 0,
`no_copy` int(1) NOT NULL DEFAULT 0,
`allow_on_submit` int(1) NOT NULL DEFAULT 0,
`show_preview_popup` int(1) NOT NULL DEFAULT 0,
`search_index` tinyint NOT NULL DEFAULT 0,
`show_dashboard` tinyint NOT NULL DEFAULT 0,
`hidden` tinyint NOT NULL DEFAULT 0,
`set_only_once` tinyint NOT NULL DEFAULT 0,
`allow_in_quick_entry` tinyint NOT NULL DEFAULT 0,
`print_hide` tinyint NOT NULL DEFAULT 0,
`report_hide` tinyint NOT NULL DEFAULT 0,
`reqd` tinyint NOT NULL DEFAULT 0,
`bold` tinyint NOT NULL DEFAULT 0,
`in_global_search` tinyint NOT NULL DEFAULT 0,
`collapsible` tinyint NOT NULL DEFAULT 0,
`unique` tinyint NOT NULL DEFAULT 0,
`no_copy` tinyint NOT NULL DEFAULT 0,
`allow_on_submit` tinyint NOT NULL DEFAULT 0,
`show_preview_popup` tinyint NOT NULL DEFAULT 0,
`trigger` varchar(255) DEFAULT NULL,
`collapsible_depends_on` text,
`mandatory_depends_on` text,
`read_only_depends_on` text,
`depends_on` text,
`permlevel` int(11) NOT NULL DEFAULT 0,
`ignore_user_permissions` int(1) NOT NULL DEFAULT 0,
`permlevel` int NOT NULL DEFAULT 0,
`ignore_user_permissions` tinyint NOT NULL DEFAULT 0,
`width` varchar(255) DEFAULT NULL,
`print_width` varchar(255) DEFAULT NULL,
`columns` int(11) NOT NULL DEFAULT 0,
`columns` int NOT NULL DEFAULT 0,
`default` text,
`description` text,
`in_list_view` int(1) NOT NULL DEFAULT 0,
`fetch_if_empty` int(1) NOT NULL DEFAULT 0,
`in_filter` int(1) NOT NULL DEFAULT 0,
`remember_last_selected_value` int(1) NOT NULL DEFAULT 0,
`ignore_xss_filter` int(1) NOT NULL DEFAULT 0,
`print_hide_if_no_value` int(1) NOT NULL DEFAULT 0,
`allow_bulk_edit` int(1) NOT NULL DEFAULT 0,
`in_standard_filter` int(1) NOT NULL DEFAULT 0,
`in_preview` int(1) NOT NULL DEFAULT 0,
`read_only` int(1) NOT NULL DEFAULT 0,
`in_list_view` tinyint NOT NULL DEFAULT 0,
`fetch_if_empty` tinyint NOT NULL DEFAULT 0,
`in_filter` tinyint NOT NULL DEFAULT 0,
`remember_last_selected_value` tinyint NOT NULL DEFAULT 0,
`ignore_xss_filter` tinyint NOT NULL DEFAULT 0,
`print_hide_if_no_value` tinyint NOT NULL DEFAULT 0,
`allow_bulk_edit` tinyint NOT NULL DEFAULT 0,
`in_standard_filter` tinyint NOT NULL DEFAULT 0,
`in_preview` tinyint NOT NULL DEFAULT 0,
`read_only` tinyint NOT NULL DEFAULT 0,
`precision` varchar(255) DEFAULT NULL,
`max_height` varchar(10) DEFAULT NULL,
`length` int(11) NOT NULL DEFAULT 0,
`translatable` int(1) NOT NULL DEFAULT 0,
`hide_border` int(1) NOT NULL DEFAULT 0,
`hide_days` int(1) NOT NULL DEFAULT 0,
`hide_seconds` int(1) NOT NULL DEFAULT 0,
`length` int NOT NULL DEFAULT 0,
`translatable` tinyint NOT NULL DEFAULT 0,
`hide_border` tinyint NOT NULL DEFAULT 0,
`hide_days` tinyint NOT NULL DEFAULT 0,
`hide_seconds` tinyint NOT NULL DEFAULT 0,
PRIMARY KEY (`name`),
KEY `parent` (`parent`),
KEY `label` (`label`),
@ -87,27 +87,27 @@ CREATE TABLE `tabDocPerm` (
`modified` datetime(6) DEFAULT NULL,
`modified_by` varchar(255) DEFAULT NULL,
`owner` varchar(255) DEFAULT NULL,
`docstatus` int(1) NOT NULL DEFAULT 0,
`docstatus` tinyint NOT NULL DEFAULT 0,
`parent` varchar(255) DEFAULT NULL,
`parentfield` varchar(255) DEFAULT NULL,
`parenttype` varchar(255) DEFAULT NULL,
`idx` int(8) NOT NULL DEFAULT 0,
`permlevel` int(11) DEFAULT '0',
`idx` int NOT NULL DEFAULT 0,
`permlevel` int DEFAULT '0',
`role` varchar(255) DEFAULT NULL,
`match` varchar(255) DEFAULT NULL,
`read` int(1) NOT NULL DEFAULT 1,
`write` int(1) NOT NULL DEFAULT 1,
`create` int(1) NOT NULL DEFAULT 1,
`submit` int(1) NOT NULL DEFAULT 0,
`cancel` int(1) NOT NULL DEFAULT 0,
`delete` int(1) NOT NULL DEFAULT 1,
`amend` int(1) NOT NULL DEFAULT 0,
`report` int(1) NOT NULL DEFAULT 1,
`export` int(1) NOT NULL DEFAULT 1,
`import` int(1) NOT NULL DEFAULT 0,
`share` int(1) NOT NULL DEFAULT 1,
`print` int(1) NOT NULL DEFAULT 1,
`email` int(1) NOT NULL DEFAULT 1,
`read` tinyint NOT NULL DEFAULT 1,
`write` tinyint NOT NULL DEFAULT 1,
`create` tinyint NOT NULL DEFAULT 1,
`submit` tinyint NOT NULL DEFAULT 0,
`cancel` tinyint NOT NULL DEFAULT 0,
`delete` tinyint NOT NULL DEFAULT 1,
`amend` tinyint NOT NULL DEFAULT 0,
`report` tinyint NOT NULL DEFAULT 1,
`export` tinyint NOT NULL DEFAULT 1,
`import` tinyint NOT NULL DEFAULT 0,
`share` tinyint NOT NULL DEFAULT 1,
`print` tinyint NOT NULL DEFAULT 1,
`email` tinyint NOT NULL DEFAULT 1,
PRIMARY KEY (`name`),
KEY `parent` (`parent`)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@ -123,11 +123,11 @@ CREATE TABLE `tabDocType Action` (
`modified` datetime(6) DEFAULT NULL,
`modified_by` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`owner` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`docstatus` int(1) NOT NULL DEFAULT 0,
`docstatus` tinyint NOT NULL DEFAULT 0,
`parent` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`parentfield` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`parenttype` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`idx` int(8) NOT NULL DEFAULT 0,
`idx` int NOT NULL DEFAULT 0,
`label` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`group` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`action_type` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
@ -147,11 +147,11 @@ CREATE TABLE `tabDocType Link` (
`modified` datetime(6) DEFAULT NULL,
`modified_by` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`owner` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`docstatus` int(1) NOT NULL DEFAULT 0,
`docstatus` tinyint NOT NULL DEFAULT 0,
`parent` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`parentfield` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`parenttype` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`idx` int(8) NOT NULL DEFAULT 0,
`idx` int NOT NULL DEFAULT 0,
`group` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`link_doctype` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`link_fieldname` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
@ -170,15 +170,15 @@ CREATE TABLE `tabDocType` (
`modified` datetime(6) DEFAULT NULL,
`modified_by` varchar(255) DEFAULT NULL,
`owner` varchar(255) DEFAULT NULL,
`docstatus` int(1) NOT NULL DEFAULT 0,
`idx` int(8) NOT NULL DEFAULT 0,
`docstatus` tinyint NOT NULL DEFAULT 0,
`idx` int NOT NULL DEFAULT 0,
`search_fields` varchar(255) DEFAULT NULL,
`issingle` int(1) NOT NULL DEFAULT 0,
`is_virtual` int(1) NOT NULL DEFAULT 0,
`is_tree` int(1) NOT NULL DEFAULT 0,
`istable` int(1) NOT NULL DEFAULT 0,
`editable_grid` int(1) NOT NULL DEFAULT 1,
`track_changes` int(1) NOT NULL DEFAULT 0,
`issingle` tinyint NOT NULL DEFAULT 0,
`is_virtual` tinyint NOT NULL DEFAULT 0,
`is_tree` tinyint NOT NULL DEFAULT 0,
`istable` tinyint NOT NULL DEFAULT 0,
`editable_grid` tinyint NOT NULL DEFAULT 1,
`track_changes` tinyint NOT NULL DEFAULT 0,
`module` varchar(255) DEFAULT NULL,
`restrict_to_domain` varchar(255) DEFAULT NULL,
`app` varchar(255) DEFAULT NULL,
@ -191,17 +191,17 @@ CREATE TABLE `tabDocType` (
`sort_order` varchar(255) DEFAULT NULL,
`description` text,
`colour` varchar(255) DEFAULT NULL,
`read_only` int(1) NOT NULL DEFAULT 0,
`in_create` int(1) NOT NULL DEFAULT 0,
`menu_index` int(11) DEFAULT NULL,
`read_only` tinyint NOT NULL DEFAULT 0,
`in_create` tinyint NOT NULL DEFAULT 0,
`menu_index` int DEFAULT NULL,
`parent_node` varchar(255) DEFAULT NULL,
`smallicon` varchar(255) DEFAULT NULL,
`allow_copy` int(1) NOT NULL DEFAULT 0,
`allow_rename` int(1) NOT NULL DEFAULT 0,
`allow_import` int(1) NOT NULL DEFAULT 0,
`hide_toolbar` int(1) NOT NULL DEFAULT 0,
`track_seen` int(1) NOT NULL DEFAULT 0,
`max_attachments` int(11) NOT NULL DEFAULT 0,
`allow_copy` tinyint NOT NULL DEFAULT 0,
`allow_rename` tinyint NOT NULL DEFAULT 0,
`allow_import` tinyint NOT NULL DEFAULT 0,
`hide_toolbar` tinyint NOT NULL DEFAULT 0,
`track_seen` tinyint NOT NULL DEFAULT 0,
`max_attachments` int NOT NULL DEFAULT 0,
`print_outline` varchar(255) DEFAULT NULL,
`document_type` varchar(255) DEFAULT NULL,
`icon` varchar(255) DEFAULT NULL,
@ -211,22 +211,22 @@ CREATE TABLE `tabDocType` (
`_last_update` varchar(32) DEFAULT NULL,
`engine` varchar(20) DEFAULT 'InnoDB',
`default_print_format` varchar(255) DEFAULT NULL,
`is_submittable` int(1) NOT NULL DEFAULT 0,
`show_name_in_global_search` int(1) NOT NULL DEFAULT 0,
`is_submittable` tinyint NOT NULL DEFAULT 0,
`show_name_in_global_search` tinyint NOT NULL DEFAULT 0,
`_user_tags` varchar(255) DEFAULT NULL,
`custom` int(1) NOT NULL DEFAULT 0,
`beta` int(1) NOT NULL DEFAULT 0,
`has_web_view` int(1) NOT NULL DEFAULT 0,
`allow_guest_to_view` int(1) NOT NULL DEFAULT 0,
`custom` tinyint NOT NULL DEFAULT 0,
`beta` tinyint NOT NULL DEFAULT 0,
`has_web_view` tinyint NOT NULL DEFAULT 0,
`allow_guest_to_view` tinyint NOT NULL DEFAULT 0,
`route` varchar(255) DEFAULT NULL,
`is_published_field` varchar(255) DEFAULT NULL,
`website_search_field` varchar(255) DEFAULT NULL,
`email_append_to` int(1) NOT NULL DEFAULT 0,
`email_append_to` tinyint NOT NULL DEFAULT 0,
`subject_field` varchar(255) DEFAULT NULL,
`sender_field` varchar(255) DEFAULT NULL,
`show_title_field_in_link` int(1) NOT NULL DEFAULT 0,
`show_title_field_in_link` tinyint NOT NULL DEFAULT 0,
`migration_hash` varchar(255) DEFAULT NULL,
`translated_doctype` int(1) NOT NULL DEFAULT 0,
`translated_doctype` tinyint NOT NULL DEFAULT 0,
PRIMARY KEY (`name`)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@ -237,7 +237,7 @@ CREATE TABLE `tabDocType` (
DROP TABLE IF EXISTS `tabSeries`;
CREATE TABLE `tabSeries` (
`name` varchar(100),
`current` int(10) NOT NULL DEFAULT 0,
`current` int NOT NULL DEFAULT 0,
PRIMARY KEY(`name`)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@ -280,7 +280,7 @@ CREATE TABLE `__Auth` (
`name` VARCHAR(255) NOT NULL,
`fieldname` VARCHAR(140) NOT NULL,
`password` TEXT NOT NULL,
`encrypted` INT(1) NOT NULL DEFAULT 0,
`encrypted` tinyint NOT NULL DEFAULT 0,
PRIMARY KEY (`doctype`, `name`, `fieldname`)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@ -295,19 +295,20 @@ CREATE TABLE `tabFile` (
`modified` datetime(6) DEFAULT NULL,
`modified_by` varchar(255) DEFAULT NULL,
`owner` varchar(255) DEFAULT NULL,
`docstatus` int(1) NOT NULL DEFAULT 0,
`docstatus` tinyint NOT NULL DEFAULT 0,
`parent` varchar(255) DEFAULT NULL,
`parentfield` varchar(255) DEFAULT NULL,
`parenttype` varchar(255) DEFAULT NULL,
`idx` int(8) NOT NULL DEFAULT 0,
`idx` int NOT NULL DEFAULT 0,
`file_name` varchar(255) DEFAULT NULL,
`file_url` varchar(255) DEFAULT NULL,
`module` varchar(255) DEFAULT NULL,
`attached_to_name` varchar(255) DEFAULT NULL,
`file_size` int(11) NOT NULL DEFAULT 0,
`file_size` int NOT NULL DEFAULT 0,
`attached_to_doctype` varchar(255) DEFAULT NULL,
PRIMARY KEY (`name`),
KEY `parent` (`parent`),
KEY `creation` (`creation`),
KEY `attached_to_name` (`attached_to_name`),
KEY `attached_to_doctype` (`attached_to_doctype`)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@ -323,11 +324,11 @@ CREATE TABLE `tabDefaultValue` (
`modified` datetime(6) DEFAULT NULL,
`modified_by` varchar(255) DEFAULT NULL,
`owner` varchar(255) DEFAULT NULL,
`docstatus` int(1) NOT NULL DEFAULT 0,
`docstatus` tinyint NOT NULL DEFAULT 0,
`parent` varchar(255) DEFAULT NULL,
`parentfield` varchar(255) DEFAULT NULL,
`parenttype` varchar(255) DEFAULT NULL,
`idx` int(8) NOT NULL DEFAULT 0,
`idx` int NOT NULL DEFAULT 0,
`defvalue` text,
`defkey` varchar(255) DEFAULT NULL,
PRIMARY KEY (`name`),

View file

@ -59,8 +59,8 @@ class MariaDBTable(DBTable):
modified datetime(6),
modified_by varchar({varchar_len}),
owner varchar({varchar_len}),
docstatus int(1) not null default '0',
idx int(8) not null default '0',
docstatus tinyint not null default '0',
idx int not null default '0',
{additional_definitions})
ENGINE={engine}
ROW_FORMAT=DYNAMIC

View file

@ -125,7 +125,7 @@ class PostgresDatabase(PostgresExceptionUtil, Database):
self.db_type = "postgres"
self.type_map = {
"Currency": ("decimal", "21,9"),
"Int": ("bigint", None),
"Int": ("int", None),
"Long Int": ("bigint", None),
"Float": ("decimal", "21,9"),
"Percent": ("decimal", "21,9"),

View file

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

View file

@ -0,0 +1,70 @@
{
"actions": [],
"allow_rename": 1,
"beta": 1,
"creation": "2023-05-16 19:37:51.047664",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"app_name",
"link",
"posting_timestamp"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "app_name",
"fieldtype": "Data",
"label": "App Name"
},
{
"fieldname": "link",
"fieldtype": "Long Text",
"in_list_view": 1,
"label": "Link",
"reqd": 1
},
{
"fieldname": "posting_timestamp",
"fieldtype": "Datetime",
"label": "Posting Timestamp",
"reqd": 1,
"search_index": 1
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-04-08 18:36:42.203032",
"modified_by": "Administrator",
"module": "Desk",
"name": "Changelog Feed",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"read_only": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View file

@ -0,0 +1,93 @@
# Copyright (c) 2023, Frappe Technologies and contributors
# For license information, please see license.txt
import requests
import frappe
from frappe.model.document import Document
from frappe.utils.caching import redis_cache
from frappe.utils.data import add_to_date
class ChangelogFeed(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
app_name: DF.Data | None
link: DF.LongText
posting_timestamp: DF.Datetime
title: DF.Data
# end: auto-generated types
pass
def fetch_changelog_feed():
"""Fetches changelog feed items from source using `get_changelog_feed` hook and stores in the db"""
since = frappe.db.get_value(
"Changelog Feed",
filters={},
fieldname="posting_timestamp",
order_by="posting_timestamp desc",
) or add_to_date(None, months=-1, as_datetime=True, as_string=False)
for fn in frappe.get_hooks("get_changelog_feed"):
try:
cache_key = f"changelog_feed::{fn}"
changelog_feed = frappe.cache.get_value(cache_key, shared=True)
if changelog_feed is None:
changelog_feed = frappe.call(fn, since=since)[:20] or []
frappe.cache.set_value(
cache_key, changelog_feed, expires_in_sec=7 * 24 * 60 * 60, shared=True
)
for feed_item in changelog_feed:
feed = {
"title": feed_item["title"],
"app_name": feed_item["app_name"],
"link": feed_item["link"],
"posting_timestamp": feed_item["creation"],
}
if not frappe.db.exists("Changelog Feed", feed):
frappe.new_doc("Changelog Feed").update(feed).insert()
except Exception:
frappe.log_error(f"Failed to fetch changelog from {fn}")
# don't retry if it's broken for 1 week
frappe.cache.set_value(cache_key, [], expires_in_sec=7 * 24 * 60 * 60, shared=True)
@redis_cache
def get_changelog_feed_items():
"""Returns a list of latest 10 changelog feed items"""
feed = frappe.get_all(
"Changelog Feed",
fields=["title", "app_name", "link", "posting_timestamp"],
# allow pubishing feed for many apps with single hook
filters={"app_name": ("in", frappe.get_installed_apps())},
order_by="posting_timestamp desc",
limit=20,
)
for f in feed:
f["app_title"] = _app_title(f["app_name"])
return feed
def _app_title(app_name):
try:
return frappe.get_hooks("app_title", app_name=app_name)[0]
except Exception:
return app_name
def get_feed(since):
"""'What's New' feed implementation for Frappe"""
r = requests.get(f"https://frappe.io/api/method/changelog_feed?since={since}")
r.raise_for_status()
return r.json()["message"]

View file

@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestChangelogFeed(FrappeTestCase):
pass

View file

@ -260,7 +260,7 @@ def get_heatmap_chart_config(chart, filters, heatmap_year):
}
def get_group_by_chart_config(chart, filters):
def get_group_by_chart_config(chart, filters) -> dict | None:
aggregate_function = get_aggregate_function(chart.group_by_type)
value_field = chart.aggregate_function_based_on or "1"
group_by_field = chart.group_by_based_on
@ -281,11 +281,10 @@ def get_group_by_chart_config(chart, filters):
if data:
return {
"labels": [item["name"] if item["name"] else "Not Specified" for item in data],
"labels": [item.get("name", "Not Specified") for item in data],
"datasets": [{"name": chart.name, "values": [item["count"] for item in data]}],
}
else:
return None
return None
def get_aggregate_function(chart_type):
@ -304,8 +303,8 @@ def get_result(data, timegrain, from_date, to_date, chart_type):
for d in result:
count = 0
while data_index < len(data) and getdate(data[data_index][0]) <= d[0]:
d[1] += data[data_index][1]
count += data[data_index][2]
d[1] += cint(data[data_index][1])
count += cint(data[data_index][2])
data_index += 1
if chart_type == "Average" and count != 0:
d[1] = d[1] / count

View file

@ -206,18 +206,22 @@ def run(
if sbool(are_default_filters) and report.custom_filters:
filters = report.custom_filters
if report.prepared_report and not sbool(ignore_prepared_report) and not custom_columns:
if filters:
if isinstance(filters, str):
filters = json.loads(filters)
try:
if report.prepared_report and not sbool(ignore_prepared_report) and not custom_columns:
if filters:
if isinstance(filters, str):
filters = json.loads(filters)
dn = filters.pop("prepared_report_name", None)
dn = filters.pop("prepared_report_name", None)
else:
dn = ""
result = get_prepared_report_result(report, filters, dn, user)
else:
dn = ""
result = get_prepared_report_result(report, filters, dn, user)
else:
result = generate_report_result(report, filters, user, custom_columns, is_tree, parent_field)
add_data_to_monitor(report=report.reference_report or report.name)
result = generate_report_result(report, filters, user, custom_columns, is_tree, parent_field)
add_data_to_monitor(report=report.reference_report or report.name)
except Exception:
frappe.log_error("Report Error")
raise
result["add_total_row"] = report.add_total_row and not result.get("skip_total_row", False)

View file

@ -122,7 +122,7 @@ class InvalidSignatureError(ValidationError):
class RateLimitExceededError(ValidationError):
pass
http_status_code = 429
class CannotChangeConstantError(ValidationError):

View file

@ -1,5 +1,13 @@
import json
EXCLUDE_SELECT_OPTIONS = [
"naming_series",
"number_format",
"float_precision",
"currency_precision",
"minimum_password_score",
]
def extract(fileobj, *args, **kwargs):
"""
@ -26,13 +34,14 @@ def extract(fileobj, *args, **kwargs):
for field in fields:
fieldtype = field.get("fieldtype")
fieldname = field.get("fieldname")
label = field.get("label")
if label:
messages.append((label, f"Label of a {fieldtype} field in DocType '{doctype}'"))
_label = label
else:
_label = field.get("fieldname")
_label = fieldname
if description := field.get("description"):
messages.append(
@ -41,6 +50,9 @@ def extract(fileobj, *args, **kwargs):
if message := field.get("options"):
if fieldtype == "Select":
if fieldname in EXCLUDE_SELECT_OPTIONS:
continue
select_options = [option for option in message.split("\n") if option and not option.isdigit()]
if select_options and "icon" in select_options[0]:

View file

@ -259,6 +259,7 @@ scheduler_events = {
"frappe.desk.form.document_follow.send_weekly_updates",
"frappe.social.doctype.energy_point_log.energy_point_log.send_weekly_summary",
"frappe.integrations.doctype.google_drive.google_drive.weekly_backup",
"frappe.desk.doctype.changelog_feed.changelog_feed.fetch_changelog_feed",
],
"monthly": [
"frappe.email.doctype.auto_email_report.auto_email_report.send_monthly",
@ -450,6 +451,8 @@ extend_bootinfo = [
"frappe.utils.sentry.add_bootinfo",
]
get_changelog_feed = "frappe.desk.doctype.changelog_feed.changelog_feed.get_feed"
export_python_type_annotations = True
standard_navbar_items = [

39798
frappe/locale/bs.po Normal file

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: developers@frappe.io\n"
"POT-Creation-Date: 2024-03-24 09:33+0000\n"
"PO-Revision-Date: 2024-03-26 12:06\n"
"PO-Revision-Date: 2024-04-04 14:56\n"
"Last-Translator: developers@frappe.io\n"
"Language-Team: German\n"
"MIME-Version: 1.0\n"
@ -2510,11 +2510,11 @@ msgstr "Änderung"
#: core/doctype/document_naming_settings/document_naming_settings.json
msgctxt "Document Naming Settings"
msgid "Amendment Naming Override"
msgstr ""
msgstr "Überschreibung der Berichtigungsbenennung"
#: core/doctype/document_naming_settings/document_naming_settings.py:207
msgid "Amendment naming rules updated."
msgstr ""
msgstr "Benennungsregeln für Berichtigungen aktualisiert."
#: public/js/frappe/ui/toolbar/toolbar.js:297
msgid "An error occurred while setting Session Defaults"
@ -2827,7 +2827,7 @@ msgstr "Genehmigung erforderlich"
#: public/js/frappe/utils/number_systems.js:41
msgctxt "Number system"
msgid "Ar"
msgstr ""
msgstr "Ar"
#: public/js/frappe/views/kanban/kanban_column.html:14
msgid "Archive"
@ -3314,7 +3314,7 @@ msgstr "Zielgruppe"
#. Name of a report
#: custom/report/audit_system_hooks/audit_system_hooks.json
msgid "Audit System Hooks"
msgstr ""
msgstr "System Hooks überprüfen"
#. Name of a DocType
#: core/doctype/audit_trail/audit_trail.json
@ -3497,11 +3497,11 @@ msgstr "Automatische Wiederholung"
#. Name of a DocType
#: automation/doctype/auto_repeat_day/auto_repeat_day.json
msgid "Auto Repeat Day"
msgstr ""
msgstr "Tag mit automatischer Wiederholung"
#: automation/doctype/auto_repeat/auto_repeat.py:159
msgid "Auto Repeat Day{0} {1} has been repeated."
msgstr ""
msgstr "Auto-Wiederholung Tag{0} {1} wurde wiederholt."
#: automation/doctype/auto_repeat/auto_repeat.py:437
msgid "Auto Repeat Document Creation Failed"
@ -8710,7 +8710,7 @@ msgstr "Lösche {0} Einträge..."
#: public/js/frappe/model/model.js:711
msgid "Deleting {0}..."
msgstr ""
msgstr "Lösche {0}..."
#. Label of a Table field in DocType 'Personal Data Deletion Request'
#: website/doctype/personal_data_deletion_request/personal_data_deletion_request.json
@ -9197,7 +9197,7 @@ msgstr "Verwerfen"
#: public/js/frappe/web_form/web_form.js:184
msgid "Discard?"
msgstr ""
msgstr "Verwerfen?"
#. Name of a DocType
#: website/doctype/discussion_reply/discussion_reply.json
@ -10601,7 +10601,7 @@ msgstr "z. B. smsgateway.com/api/send_sms.cgi"
#: rate_limiter.py:139
msgid "Either key or IP flag is required."
msgstr ""
msgstr "Entweder der key (Schlüssel) oder das IP-Flag ist erforderlich."
#. Label of a Data field in DocType 'Form Tour Step'
#: desk/doctype/form_tour_step/form_tour_step.json
@ -13573,7 +13573,7 @@ msgstr "Formular-Generator"
#: core/doctype/recorder/recorder.json
msgctxt "Recorder"
msgid "Form Dict"
msgstr ""
msgstr "Formular Dict"
#. Label of a Section Break field in DocType 'Customize Form'
#: custom/doctype/customize_form/customize_form.json
@ -13617,19 +13617,19 @@ msgstr "Formular URL-verschlüsselt"
#: public/js/frappe/widgets/widget_dialog.js:567
msgid "Format"
msgstr ""
msgstr "Formatierung"
#. Label of a Select field in DocType 'Auto Email Report'
#: email/doctype/auto_email_report/auto_email_report.json
msgctxt "Auto Email Report"
msgid "Format"
msgstr ""
msgstr "Formatierung"
#. Label of a Data field in DocType 'Workspace Shortcut'
#: desk/doctype/workspace_shortcut/workspace_shortcut.json
msgctxt "Workspace Shortcut"
msgid "Format"
msgstr ""
msgstr "Formatierung"
#. Label of a Code field in DocType 'Print Format'
#: printing/doctype/print_format/print_format.json
@ -17004,7 +17004,7 @@ msgstr "Ist Abfragebericht"
#: integrations/doctype/integration_request/integration_request.json
msgctxt "Integration Request"
msgid "Is Remote Request?"
msgstr ""
msgstr "Ist dies eine Remote-Anfrage?"
#: core/doctype/doctype/doctype_list.js:64
msgid "Is Single"
@ -18269,7 +18269,7 @@ msgstr "Like Limit pro Stunde"
#: templates/includes/likes/likes.py:30
msgid "Like on {0}: {1}"
msgstr ""
msgstr "„Gefällt mir“ für {0}: {1}"
#: desk/like.py:91
msgid "Liked"
@ -21219,7 +21219,7 @@ msgstr "Noch keine Kommentare"
#: templates/includes/comments/comments.html:4
msgid "No comments yet. "
msgstr ""
msgstr "Noch keine Kommentare. "
#: public/js/frappe/form/templates/contact_list.html:85
msgid "No contacts added yet."
@ -23161,7 +23161,7 @@ msgstr "Passwort überschreitet die maximal zulässige Länge."
#: www/update-password.html:78
msgid "Passwords do not match"
msgstr ""
msgstr "Passwörter stimmen nicht überein"
#: core/doctype/user/user.js:191
msgid "Passwords do not match!"
@ -25367,13 +25367,13 @@ msgstr "Erneut in der Konsole ausführen"
#: email/doctype/email_account/email_account.py:660
msgid "Re:"
msgstr ""
msgstr "AW:"
#: core/doctype/communication/communication.js:268
#: public/js/frappe/form/footer/form_timeline.js:587
#: public/js/frappe/views/communication.js:347
msgid "Re: {0}"
msgstr ""
msgstr "AW: {0}"
#: client.py:459
msgid "Read"
@ -25622,12 +25622,12 @@ msgstr "Empfänger"
#. Name of a DocType
#: core/doctype/recorder/recorder.json
msgid "Recorder"
msgstr ""
msgstr "Rekorder"
#. Name of a DocType
#: core/doctype/recorder_query/recorder_query.json
msgid "Recorder Query"
msgstr ""
msgstr "Rekorder-Abfrage"
#: core/doctype/user_permission/user_permission_help.html:2
msgid "Records for following doctypes will be filtered"
@ -26844,7 +26844,7 @@ msgstr "Benötigt einen gültigen fdn-Pfad. z.B. ou=users,dc=example,dc=com"
#: core/doctype/communication/communication.js:279
msgid "Res: {0}"
msgstr ""
msgstr "AW: {0}"
#: desk/doctype/form_tour/form_tour.js:101
#: desk/doctype/global_search_settings/global_search_settings.js:19
@ -26955,7 +26955,7 @@ msgstr "Antwort"
#: email/doctype/email_template/email_template.json
msgctxt "Email Template"
msgid "Response "
msgstr ""
msgstr "Antwort "
#. Label of a Select field in DocType 'OAuth Client'
#: integrations/doctype/oauth_client/oauth_client.json
@ -30631,13 +30631,13 @@ msgstr "Pos"
#: core/doctype/recorder/recorder.js:33
msgid "Stack Trace"
msgstr ""
msgstr "Stack Trace"
#. Label of a HTML field in DocType 'Recorder Query'
#: core/doctype/recorder_query/recorder_query.json
msgctxt "Recorder Query"
msgid "Stack Trace"
msgstr ""
msgstr "Stack Trace"
#: core/doctype/user_type/user_type_list.js:5
msgid "Standard"
@ -31264,7 +31264,7 @@ msgstr "Betreff Der Feldtyp sollte Daten, Text, Langtext, Kleiner Text, Textedit
#. Name of a DocType
#: core/doctype/submission_queue/submission_queue.json
msgid "Submission Queue"
msgstr ""
msgstr "Buchungs-Warteschlange"
#: core/doctype/user_permission/user_permission_list.js:138
#: public/js/frappe/form/quick_entry.js:198
@ -33281,19 +33281,19 @@ msgstr "Titel der Seite"
#: public/js/frappe/views/communication.js:53
#: public/js/frappe/views/inbox/inbox_view.js:70
msgid "To"
msgstr "Zu"
msgstr "An"
#. Label of a Code field in DocType 'Communication'
#: core/doctype/communication/communication.json
msgctxt "Communication"
msgid "To"
msgstr "Zu"
msgstr "An"
#. Label of a Section Break field in DocType 'Newsletter'
#: email/doctype/newsletter/newsletter.json
msgctxt "Newsletter"
msgid "To"
msgstr "Zu"
msgstr "An"
#: website/report/website_analytics/website_analytics.js:14
msgid "To Date"
@ -33706,7 +33706,7 @@ msgstr "E-Mail-Status verfolgen"
#: automation/doctype/milestone/milestone.json
msgctxt "Milestone"
msgid "Track Field"
msgstr ""
msgstr "Verfolge Feld"
#. Label of a Check field in DocType 'DocType'
#: core/doctype/doctype/doctype.json
@ -35694,7 +35694,7 @@ msgstr "Tutorial ansehen"
#: desk/doctype/onboarding_step/onboarding_step.json
msgctxt "Onboarding Step"
msgid "Watch Video"
msgstr "Schau Video"
msgstr "Video ansehen"
#: desk/doctype/workspace/workspace.js:38
msgid "We do not allow editing of this document. Simply click the Edit button on the workspace page to make your workspace editable and customize it as you wish"
@ -35963,13 +35963,13 @@ msgstr "Website-Meta-Tag"
#. Name of a DocType
#: website/doctype/website_route_meta/website_route_meta.json
msgid "Website Route Meta"
msgstr ""
msgstr "Meta-Tags für Website-Pfad"
#. Label of a Link in the Website Workspace
#: website/workspace/website/website.json
msgctxt "Website Route Meta"
msgid "Website Route Meta"
msgstr ""
msgstr "Meta-Tags für Website-Pfad"
#. Name of a DocType
#: website/doctype/website_route_redirect/website_route_redirect.json
@ -37372,7 +37372,7 @@ msgstr "Der Parameter `job_id` ist für die Deduplizierung erforderlich."
#: public/js/frappe/form/footer/version_timeline_content_builder.js:219
msgid "added rows for {0}"
msgstr "Zeilen für {0} hinzugefügt"
msgstr "hat Zeilen für {0} hinzugefügt"
#. Option for the 'Icon' (Select) field in DocType 'Workflow State'
#: workflow/doctype/workflow_state/workflow_state.json
@ -37415,7 +37415,7 @@ msgstr "rechtsbündig"
#: core/doctype/permission_inspector/permission_inspector.json
msgctxt "Permission Inspector"
msgid "amend"
msgstr ""
msgstr "berichtigen"
#: public/js/frappe/utils/utils.js:396 utils/data.py:1504
msgid "and"
@ -37551,13 +37551,13 @@ msgstr "Kamera"
#: core/doctype/permission_inspector/permission_inspector.json
msgctxt "Permission Inspector"
msgid "cancel"
msgstr ""
msgstr "stornieren"
#. Option for the 'Status' (Select) field in DocType 'RQ Job'
#: core/doctype/rq_job/rq_job.json
msgctxt "RQ Job"
msgid "canceled"
msgstr ""
msgstr "storniert"
#. Option for the 'Icon' (Select) field in DocType 'Workflow State'
#: workflow/doctype/workflow_state/workflow_state.json
@ -37644,7 +37644,7 @@ msgstr "kommentierte(n)"
#: core/doctype/permission_inspector/permission_inspector.json
msgctxt "Permission Inspector"
msgid "create"
msgstr ""
msgstr "erstellen"
#. Option for the 'Indicator Color' (Select) field in DocType 'Workspace'
#: desk/doctype/workspace/workspace.json
@ -37708,7 +37708,7 @@ msgstr "aufgeschoben"
#: core/doctype/permission_inspector/permission_inspector.json
msgctxt "Permission Inspector"
msgid "delete"
msgstr ""
msgstr "löschen"
#: public/js/frappe/ui/sort_selector.html:5
#: public/js/frappe/ui/sort_selector.js:48
@ -37860,7 +37860,7 @@ msgstr "Apple FaceTime-Video"
#: core/doctype/rq_job/rq_job.json
msgctxt "RQ Job"
msgid "failed"
msgstr ""
msgstr "fehlgeschlagen"
#. Option for the 'Social Login Provider' (Select) field in DocType 'Social
#. Login Key'
@ -37903,7 +37903,7 @@ msgstr "Filter"
#: core/doctype/rq_job/rq_job.json
msgctxt "RQ Job"
msgid "finished"
msgstr ""
msgstr "fertig"
#. Option for the 'Icon' (Select) field in DocType 'Workflow State'
#: workflow/doctype/workflow_state/workflow_state.json
@ -38495,7 +38495,7 @@ msgstr "zufällig"
#: core/doctype/permission_inspector/permission_inspector.json
msgctxt "Permission Inspector"
msgid "read"
msgstr ""
msgstr "lesen"
#. Option for the 'Indicator Color' (Select) field in DocType 'Workspace'
#: desk/doctype/workspace/workspace.json
@ -38529,7 +38529,7 @@ msgstr "Entfernen-Zeichen"
#: public/js/frappe/form/footer/version_timeline_content_builder.js:221
msgid "removed rows for {0}"
msgstr "entfernte Zeilen für {0}"
msgstr "hat Zeilen von {0} entfernt"
#: model/rename_doc.py:214
msgid "renamed from {0} to {1}"
@ -38610,7 +38610,7 @@ msgstr "s256"
#: core/doctype/rq_job/rq_job.json
msgctxt "RQ Job"
msgid "scheduled"
msgstr ""
msgstr "geplant"
#. Option for the 'Icon' (Select) field in DocType 'Workflow State'
#: workflow/doctype/workflow_state/workflow_state.json
@ -38629,7 +38629,7 @@ msgstr "Suche"
#: core/doctype/permission_inspector/permission_inspector.json
msgctxt "Permission Inspector"
msgid "select"
msgstr ""
msgstr "auswählen"
#. Option for the 'Permission Type' (Select) field in DocType 'Permission
#. Inspector'
@ -38706,7 +38706,7 @@ msgstr "sternenleer"
#: core/doctype/rq_job/rq_job.json
msgctxt "RQ Job"
msgid "started"
msgstr ""
msgstr "gestartet"
#: desk/page/setup_wizard/setup_wizard.js:194
msgid "starting the setup..."
@ -38756,7 +38756,7 @@ msgstr "String-Wert, z.B {0} oder uid={0},ou=users,dc=example,dc=com"
#: core/doctype/permission_inspector/permission_inspector.json
msgctxt "Permission Inspector"
msgid "submit"
msgstr ""
msgstr "buchen"
#. Option for the 'Icon' (Select) field in DocType 'Workflow State'
#: workflow/doctype/workflow_state/workflow_state.json
@ -38962,7 +38962,7 @@ msgstr "Schraubenschlüssel"
#: core/doctype/permission_inspector/permission_inspector.json
msgctxt "Permission Inspector"
msgid "write"
msgstr ""
msgstr "schreiben"
#. Option for the 'Indicator Color' (Select) field in DocType 'Workspace'
#: desk/doctype/workspace/workspace.json

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -983,6 +983,9 @@ class BaseDocument:
self.throw_length_exceeded_error(df, max_length, value)
elif column_type in ("int", "bigint", "smallint"):
if cint(df.get("length")) > 11: # We implicitl switch to bigint for >11
column_type = "bigint"
max_length = max_positive_value[column_type]
if abs(cint(value)) > max_length:

View file

@ -26,6 +26,8 @@ from frappe.utils.data import get_absolute_url, get_datetime, get_timedelta, get
from frappe.utils.global_search import update_global_search
if TYPE_CHECKING:
from typing_extensions import Self
from frappe.core.doctype.docfield.docfield import DocField
@ -144,7 +146,7 @@ class Document(BaseDocument):
def is_locked(self):
return file_lock.lock_exists(self.get_signature())
def load_from_db(self):
def load_from_db(self) -> "Self":
"""Load document and children from database and create properties
from fields"""
self.flags.ignore_children = True
@ -204,7 +206,7 @@ class Document(BaseDocument):
return self
def reload(self):
def reload(self) -> "Self":
"""Reload document from database"""
return self.load_from_db()
@ -248,7 +250,7 @@ class Document(BaseDocument):
ignore_mandatory=None,
set_name=None,
set_child_names=True,
) -> "Document":
) -> "Self":
"""Insert the document in the database (as a new document).
This will check for user permissions and execute `before_insert`,
`validate`, `on_update`, `after_insert` methods if they are written.
@ -333,11 +335,11 @@ class Document(BaseDocument):
if self.creation and self.is_locked:
raise frappe.DocumentLockedError
def save(self, *args, **kwargs):
def save(self, *args, **kwargs) -> "Self":
"""Wrapper for _save"""
return self._save(*args, **kwargs)
def _save(self, ignore_permissions=None, ignore_version=None) -> "Document":
def _save(self, ignore_permissions=None, ignore_version=None) -> "Self":
"""Save the current document in the database in the **DocType**'s table or
`tabSingles` (for single types).

View file

@ -332,11 +332,6 @@ def has_user_permission(doc, user=None, debug=False):
debug and _debug_log("User is not affected by any user permissions")
return True
# user can create own role permissions, so nothing applies
if get_role_permissions("User Permission", user=user).get("write"):
debug and _debug_log("User permission bypassed because user can modify user permissions.")
return True
# don't apply strict user permissions for single doctypes since they contain empty link fields
apply_strict_user_permissions = (
False if doc.meta.issingle else frappe.get_system_settings("apply_strict_user_permissions")

View file

@ -24,6 +24,13 @@ frappe.ui.form.ControlTable = class ControlTable extends frappe.ui.form.Control
const doctype = grid.doctype;
const row_docname = $(e.target).closest(".grid-row").data("name");
const in_grid_form = $(e.target).closest(".form-in-grid").length;
const value_formatter_map = {
Date: (val) => (val ? frappe.datetime.user_to_str(val) : val),
Int: (val) => cint(val),
Check: (val) => cint(val),
Float: (val) => flt(val),
Currency: (val) => flt(val),
};
let pasted_data = frappe.utils.get_clipboard_data(e);
@ -34,10 +41,13 @@ frappe.ui.form.ControlTable = class ControlTable extends frappe.ui.form.Control
if (data.length === 1 && data[0].length === 1) return;
let fieldnames = [];
let fieldtypes = [];
// for raw data with column header
if (this.get_field(data[0][0])) {
data[0].forEach((column) => {
fieldnames.push(this.get_field(column));
const df = frappe.meta.get_docfield(doctype, this.get_field(column));
fieldtypes.push(df ? df.fieldtype : "");
});
data.shift();
} else {
@ -51,6 +61,8 @@ frappe.ui.form.ControlTable = class ControlTable extends frappe.ui.form.Control
column.fieldname === $(e.target).data("fieldname")
) {
fieldnames.push(column.fieldname);
const df = frappe.meta.get_docfield(doctype, column.fieldname);
fieldtypes.push(df ? df.fieldtype : "");
target_column_matched = true;
}
});
@ -73,6 +85,10 @@ frappe.ui.form.ControlTable = class ControlTable extends frappe.ui.form.Control
const row_name = grid_rows[row_idx - 1].doc.name;
row.forEach((value, data_index) => {
if (fieldnames[data_index]) {
// format value before setting
value = value_formatter_map[fieldtypes[data_index]]
? value_formatter_map[fieldtypes[data_index]](value)
: value;
frappe.model.set_value(
doctype,
row_name,

View file

@ -1730,17 +1730,19 @@ frappe.ui.form.Form = class FrappeForm {
if (opts.child) {
// update child doc
opts.child = locals[opts.child.doctype][opts.child.name];
var std_field_list = ["doctype"]
.concat(frappe.model.std_fields_list)
.concat(frappe.model.child_table_field_list);
for (var key in r.message) {
if (std_field_list.indexOf(key) === -1) {
opts.child[key] = r.message[key];
// if child row is deleted, don't update
if (opts.child) {
var std_field_list = ["doctype"]
.concat(frappe.model.std_fields_list)
.concat(frappe.model.child_table_field_list);
for (var key in r.message) {
if (std_field_list.indexOf(key) === -1) {
opts.child[key] = r.message[key];
}
}
}
me.fields_dict[opts.child.parentfield].refresh();
me.fields_dict[opts.child.parentfield].refresh();
}
} else {
// update parent doc
me.set_value(r.message);

View file

@ -236,7 +236,7 @@ export default class Grid {
this.df.data = this.get_data();
this.df.data = this.df.data.filter((row) => row.idx != doc.idx);
}
this.grid_rows_by_docname[doc.name].remove();
this.grid_rows_by_docname[doc.name]?.remove();
dirty = true;
});
tasks.push(() => frappe.timeout(0.1));
@ -795,7 +795,7 @@ export default class Grid {
}
set_value(fieldname, value, doc) {
if (this.display_status !== "None" && this.grid_rows_by_docname[doc.name]) {
if (this.display_status !== "None" && doc.name && this.grid_rows_by_docname[doc.name]) {
this.grid_rows_by_docname[doc.name].refresh_field(fieldname, value);
}
}

View file

@ -212,6 +212,10 @@ frappe.ui.form.Layout = class Layout {
const parent = this.column.form.get(0);
const fieldobj = this.init_field(df, parent, render);
// An invalid control name will return in a null fieldobj
if (!fieldobj) return;
this.fields_list.push(fieldobj);
this.fields_dict[df.fieldname] = fieldobj;
@ -234,7 +238,11 @@ frappe.ui.form.Layout = class Layout {
layout: this,
});
fieldobj.layout = this;
// make_control can return null for invalid control names
if (fieldobj) {
fieldobj.layout = this;
}
return fieldobj;
}

View file

@ -272,7 +272,7 @@ frappe.views.ListSidebar = class ListSidebar {
const message = __("Get more insights with");
const link = "https://frappe.io/s/insights";
const cta = __("Frappe Insights");
const cta = "Frappe Insights";
this.insights_banner = $(`
<div style="position: relative;">

View file

@ -2042,7 +2042,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
};
// bulk edit
if (has_editable_fields(doctype)) {
if (has_editable_fields(doctype) && !frappe.model.has_workflow(doctype)) {
actions_menu_items.push(bulk_edit());
}
@ -2077,7 +2077,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}
// bulk delete
if (frappe.model.can_delete(doctype)) {
if (frappe.model.can_delete(doctype) && !frappe.model.has_workflow(doctype)) {
actions_menu_items.push(bulk_delete());
}

View file

@ -59,10 +59,9 @@ frappe.workflow = {
var state =
doc[state_fieldname] || frappe.workflow.get_default_state(doctype, doc.docstatus);
if (!state) return false;
let allow_edit_roles = state
? frappe.workflow.get_document_state_roles(doctype, state)
: null;
let allow_edit_roles = frappe.workflow.get_document_state_roles(doctype, state);
let has_common_role = frappe.user_roles.some((role) =>
allow_edit_roles.includes(role)
);

View file

@ -152,6 +152,11 @@ frappe.ui.keys.AltShortcutGroup = class AltShortcutGroup {
}
underline_text(shortcut) {
if (frappe.boot.lang === "eo") {
// The language code "eo" is used to trigger the In-Context Translation feature.
// In this case we don't want shortcuts to rip apart the ID of the translatable text.
return;
}
shortcut.$text_el.attr("data-label", encodeURIComponent(shortcut.text));
let underline_el_found = false;
let text_html = shortcut.text

View file

@ -248,7 +248,7 @@ frappe.ui.Filter = class {
let args = {};
if (this.filters_config[condition].depends_on) {
const field_name = this.filters_config[condition].depends_on;
const filter_value = this.filter_list.get_filter_value(fieldname);
const filter_value = this.filter_list.get_filter_value(field_name);
args[field_name] = filter_value;
}
let setup_field = (field) => {
@ -424,6 +424,12 @@ frappe.ui.filter_utils = {
let val = field.get_value() ?? field.value;
if (!val && ["Link", "Dynamic Link"].includes(field.df.fieldtype)) {
// HACK: link field with show title are async so their input value is "" but they have
// some actual value set.
val = field.value;
}
if (typeof val === "string") {
val = strip(val);
}

View file

@ -15,6 +15,7 @@ frappe.ui.Notifications = class Notifications {
this.body = this.dropdown_list.find(".notification-list-body");
this.panel_events = this.dropdown_list.find(".panel-events");
this.panel_notifications = this.dropdown_list.find(".panel-notifications");
this.panel_changelog_feed = this.dropdown_list.find(".panel-changelog-feed");
this.user = frappe.session.user;
@ -52,11 +53,17 @@ frappe.ui.Notifications = class Notifications {
el: this.panel_notifications,
},
{
label: __("Today's Events"),
label: __("Events"),
id: "todays_events",
view: EventsView,
el: this.panel_events,
},
{
label: __("What's New"),
id: "changelog_feed",
view: ChangelogFeedView,
el: this.panel_changelog_feed,
},
];
let get_headers_html = (item) => {
@ -439,3 +446,53 @@ class EventsView extends BaseNotificationsView {
this.container.html(html);
}
}
class ChangelogFeedView extends BaseNotificationsView {
make() {
this.render_changelog_feed_html(frappe.boot.changelog_feed || []);
}
render_changelog_feed_html(changelog_feed) {
let html = "";
if (changelog_feed.length) {
this.container.empty();
const get_changelog_feed_html = (changelog_feed_item) => {
const timestamp = frappe.datetime.prettyDate(
changelog_feed_item.posting_timestamp
);
const message_html = `<div class="message">
<div>${changelog_feed_item.title}</div>
<div class="notification-timestamp text-muted">
${changelog_feed_item.app_title} | ${timestamp}
</div>
</div>`;
const item_html = `<a class="recent-item notification-item"
href="${changelog_feed_item.link}"
data-name="${changelog_feed_item.title}"
target="_blank" rel="noopener noreferrer"
>
<div class="notification-body">
${message_html}
</div>
</div>
</a>`;
return item_html;
};
html = changelog_feed.map(get_changelog_feed_html).join("");
} else {
html = `<div class="notification-null-state">
<div class="text-center">
<img src="/assets/frappe/images/ui-states/notification-empty-state.svg" alt="Generic Empty State" class="null-state">
<div class="title">${__("Nothing New")}</div>
<div class="subtitle">
${__("There is nothing new to show you right now.")}
</div>
</div>
</div>
`;
}
this.container.html(html);
}
}

View file

@ -17,7 +17,7 @@
</button>
<div class="flex fill-width title-area">
<div>
<div class="flex">
<div class="flex col-md-8 col-sm-8">
<h3 class="ellipsis title-text"></h3>
<span class="indicator-pill whitespace-nowrap"></span>
</div>

View file

@ -24,6 +24,13 @@ frappe.ui.misc.about = function () {
<hr>
<h4>${__("Installed Apps")}</h4>
<div id='about-app-versions'>${__("Loading versions...")}</div>
<p>
<b>
<a href="/attribution" target="_blank" class="text-muted">
${__("Dependencies & Licenses")}
</a>
</b>
</p>
<hr>
<p class='text-muted'>${__("&copy; Frappe Technologies Pvt. Ltd. and contributors")} </p>
</div>`,

View file

@ -1,139 +1,154 @@
<header class="navbar navbar-expand sticky-top" role="navigation">
<div class="container">
<a class="navbar-brand navbar-home" href="/app">
<img
class="app-logo"
style="width: {{ navbar_settings.logo_width || 28 }}px"
src="{{ frappe.boot.app_logo_url }}"
alt="{{ __("App Logo") }}"
>
</a>
<ul class="nav navbar-nav d-none d-sm-flex" id="navbar-breadcrumbs"></ul>
<div class="collapse navbar-collapse justify-content-end">
<form class="form-inline fill-width justify-content-end" role="search" onsubmit="return false;">
{% if (frappe.boot.read_only) { %}
<span class="indicator-pill yellow no-indicator-dot read-only-banner" title="{%= __("Your site is undergoing maintenance or being updated.") %}">
{%= __("Read Only Mode") %}
</span>
{% } %}
{% if (frappe.boot.user.impersonated_by) { %}
<span class="indicator-pill red no-indicator-dot" title="{%= __("You are impersonating as another user.") %}">
{%= __("Impersonating {0}", [frappe.boot.user.name]) %}
</span>
{% } %}
<div class="input-group search-bar text-muted hidden">
<input
id="navbar-search"
type="text"
class="form-control"
placeholder="{%= __('Search or type a command ({0})', [frappe.utils.is_mac() ? '⌘ + G' : 'Ctrl + G']) %}"
aria-haspopup="true"
>
<span class="search-icon">
<svg class="icon icon-sm"><use href="#icon-search"></use></svg>
</span>
</div>
</form>
<ul class="navbar-nav">
<li class="nav-item dropdown dropdown-notifications dropdown-mobile hidden">
<button
class="btn-reset nav-link notifications-icon text-muted"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
<span class="notifications-seen">
<span class="sr-only">{{ __("No new notifications") }}</span>
<svg class="es-icon icon-sm" style="stroke:none;"><use href="#es-line-notifications"></use></svg>
<div class="sticky-top">
<header class="navbar navbar-expand" role="navigation">
<div class="container">
<a class="navbar-brand navbar-home" href="/app">
<img
class="app-logo"
style="width: {{ navbar_settings.logo_width || 28 }}px"
src="{{ frappe.boot.app_logo_url }}"
alt="{{ __("App Logo") }}"
>
</a>
<ul class="nav navbar-nav d-none d-sm-flex" id="navbar-breadcrumbs"></ul>
<div class="collapse navbar-collapse justify-content-end">
<form class="form-inline fill-width justify-content-end" role="search" onsubmit="return false;">
{% if (frappe.boot.read_only) { %}
<span class="indicator-pill yellow no-indicator-dot read-only-banner" title="{%= __("Your site is undergoing maintenance or being updated.") %}">
{%= __("Read Only Mode") %}
</span>
<span class="notifications-unseen">
<span class="sr-only">{{ __("You have unseen notifications") }}</span>
<svg class="es-icon icon-sm"><use href="#es-line-notifications-unseen"></use></svg>
{% } %}
{% if (frappe.boot.user.impersonated_by) { %}
<span class="indicator-pill red no-indicator-dot" title="{%= __("You are impersonating as another user.") %}">
{%= __("Impersonating {0}", [frappe.boot.user.name]) %}
</span>
</button>
<div class="dropdown-menu notifications-list dropdown-menu-right" role="menu">
<div class="notification-list-header">
<div class="header-items"></div>
<div class="header-actions"></div>
{% } %}
<div class="input-group search-bar text-muted hidden">
<input
id="navbar-search"
type="text"
class="form-control"
placeholder="{%= __('Search or type a command ({0})', [frappe.utils.is_mac() ? '⌘ + G' : 'Ctrl + G']) %}"
aria-haspopup="true"
>
<span class="search-icon">
<svg class="icon icon-sm"><use href="#icon-search"></use></svg>
</span>
</div>
</form>
<ul class="navbar-nav">
<li class="nav-item dropdown dropdown-notifications dropdown-mobile hidden">
<button
class="btn-reset nav-link notifications-icon text-muted"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
<span class="notifications-seen">
<span class="sr-only">{{ __("No new notifications") }}</span>
<svg class="es-icon icon-sm" style="stroke:none;"><use href="#es-line-notifications"></use></svg>
</span>
<span class="notifications-unseen">
<span class="sr-only">{{ __("You have unseen notifications") }}</span>
<svg class="es-icon icon-sm"><use href="#es-line-notifications-unseen"></use></svg>
</span>
</button>
<div class="dropdown-menu notifications-list dropdown-menu-right" role="menu">
<div class="notification-list-header">
<div class="header-items"></div>
<div class="header-actions"></div>
</div>
<div class="notification-list-body">
<div class="panel-notifications"></div>
<div class="panel-events"></div>
<div class="panel-changelog-feed"></div>
</div>
</div>
<div class="notification-list-body">
<div class="panel-notifications"></div>
<div class="panel-events"></div>
</li>
<li class="nav-item dropdown dropdown-message dropdown-mobile hidden">
<button
class="btn-reset nav-link notifications-icon text-muted"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="true"
>
<span>
<svg class="es-icon icon-sm"><use href="#es-line-chat-alt"></use></svg>
</span>
</button>
</li>
<li class="vertical-bar d-none d-sm-block"></li>
<li class="nav-item dropdown dropdown-help dropdown-mobile d-none d-lg-block">
<button
class="btn-reset nav-link"
data-toggle="dropdown"
aria-controls="toolbar-help"
aria-label="{{ __("Help Dropdown") }}"
>
<span>
{{ __("Help") }}
<svg class="es-icon icon-xs"><use href="#es-line-down"></use></svg>
</span>
</button>
<div class="dropdown-menu dropdown-menu-right" id="toolbar-help" role="menu">
<div id="help-links"></div>
<div class="dropdown-divider documentation-links"></div>
{% for item in navbar_settings.help_dropdown %}
{% if (!item.hidden) { %}
{% if (item.route) { %}
<a class="dropdown-item" href="{{ item.route }}">
{%= __(item.item_label) %}
</a>
{% } else if (item.action) { %}
<button class="btn-reset dropdown-item" onclick="return {{ item.action }}">
{%= __(item.item_label) %}
</button>
{% } else { %}
<div class="dropdown-divider"></div>
{% } %}
{% } %}
{% endfor %}
</div>
</div>
</li>
<li class="nav-item dropdown dropdown-message dropdown-mobile hidden">
<button
class="btn-reset nav-link notifications-icon text-muted"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="true"
>
<span>
<svg class="es-icon icon-sm"><use href="#es-line-chat-alt"></use></svg>
</span>
</button>
</li>
<li class="vertical-bar d-none d-sm-block"></li>
<li class="nav-item dropdown dropdown-help dropdown-mobile d-none d-lg-block">
<button
class="btn-reset nav-link"
data-toggle="dropdown"
aria-controls="toolbar-help"
aria-label="{{ __("Help Dropdown") }}"
>
<span>
{{ __("Help") }}
<svg class="es-icon icon-xs"><use href="#es-line-down"></use></svg>
</span>
</button>
<div class="dropdown-menu dropdown-menu-right" id="toolbar-help" role="menu">
<div id="help-links"></div>
<div class="dropdown-divider documentation-links"></div>
{% for item in navbar_settings.help_dropdown %}
{% if (!item.hidden) { %}
{% if (item.route) { %}
<a class="dropdown-item" href="{{ item.route }}">
{%= __(item.item_label) %}
</a>
{% } else if (item.action) { %}
<button class="btn-reset dropdown-item" onclick="return {{ item.action }}">
{%= __(item.item_label) %}
</button>
{% } else { %}
<div class="dropdown-divider"></div>
</li>
<li class="nav-item dropdown dropdown-navbar-user dropdown-mobile">
<button
class="btn-reset nav-link"
data-toggle="dropdown"
aria-label="{{ __("User Menu") }}"
>
{{ avatar }}
</button>
<div class="dropdown-menu dropdown-menu-right" id="toolbar-user" role="menu">
{% for item in navbar_settings.settings_dropdown %}
{% if (!item.hidden) { %}
{% if (item.route) { %}
<a class="dropdown-item" href="{{ item.route }}">
{%= __(item.item_label) %}
</a>
{% } else if (item.action) { %}
<button class="btn-reset dropdown-item" onclick="return {{ item.action }}">
{%= __(item.item_label) %}
</button>
{% } else { %}
<div class="dropdown-divider"></div>
{% } %}
{% } %}
{% } %}
{% endfor %}
</div>
</li>
<li class="nav-item dropdown dropdown-navbar-user dropdown-mobile">
<button
class="btn-reset nav-link"
data-toggle="dropdown"
aria-label="{{ __("User Menu") }}"
>
{{ avatar }}
</button>
<div class="dropdown-menu dropdown-menu-right" id="toolbar-user" role="menu">
{% for item in navbar_settings.settings_dropdown %}
{% if (!item.hidden) { %}
{% if (item.route) { %}
<a class="dropdown-item" href="{{ item.route }}">
{%= __(item.item_label) %}
</a>
{% } else if (item.action) { %}
<button class="btn-reset dropdown-item" onclick="return {{ item.action }}">
{%= __(item.item_label) %}
</button>
{% } else { %}
<div class="dropdown-divider"></div>
{% } %}
{% } %}
{% endfor %}
</div>
</li>
</ul>
{% endfor %}
</div>
</li>
</ul>
</div>
</div>
</header>
{% if !localStorage.getItem("dismissed_announcement_widget") && strip_html(navbar_settings.announcement_widget) != '' %}
<div class="announcement-widget form-message p-2 m-0" style="position: relative; z-index: -1; border-radius: 0; background-color: var(--bg-blue);">
<div class="container flex justify-between align-center mx-auto">
{{ navbar_settings.announcement_widget }}
<div class="close-message p-0 mr-2" style="position: relative;">
{{ frappe.utils.icon("close") }}
</div>
</div>
</div>
</header>
{% endif %}
</div>

View file

@ -21,6 +21,7 @@ frappe.ui.toolbar.Toolbar = class {
this.setup_notifications();
this.setup_help();
this.setup_read_only_mode();
this.setup_announcement_widget();
this.make();
}
@ -56,6 +57,30 @@ frappe.ui.toolbar.Toolbar = class {
});
}
setup_announcement_widget() {
let current_announcement = frappe.boot.navbar_settings.announcement_widget;
if (!current_announcement) return;
// If an unseen announcement is added, overlook dismiss flag
if (current_announcement != localStorage.getItem("announcement_widget")) {
localStorage.removeItem("dismissed_announcement_widget");
localStorage.setItem("announcement_widget", current_announcement);
}
// When an announcement is closed, add dismiss flag
if (!localStorage.getItem("dismissed_announcement_widget")) {
let announcement_widget = $(".announcement-widget");
let close_message = announcement_widget.find(".close-message");
close_message.on(
"click",
() =>
localStorage.setItem("dismissed_announcement_widget", true) ||
announcement_widget.addClass("hidden")
);
}
}
setup_help() {
if (!frappe.boot.desk_settings.notifications) {
// hide the help section

View file

@ -98,7 +98,9 @@ export default class Widget {
let base = this.title || this.label || this.name;
let title = max_chars ? frappe.ellipsis(base, max_chars) : base;
this.title_field[0].innerHTML = `<span class="ellipsis" title="${title}">${title}</span>`;
this.title_field[0].innerHTML = `<span class="ellipsis" title="${__(title)}">${__(
title
)}</span>`;
if (max_chars) {
this.title_field[0].setAttribute("title", this.title || this.label);
}

View file

@ -292,6 +292,12 @@
}
}
.ql-editor {
p:not(:first-child) {
margin-top: 0.75rem;
}
}
.ql-editor td {
border: 1px solid var(--dark-border-color);
}

View file

@ -147,7 +147,8 @@ def rate_limit(
value = frappe.cache.incrby(cache_key, 1)
if value > _limit:
frappe.throw(
_("You hit the rate limit because of too many requests. Please try after sometime.")
_("You hit the rate limit because of too many requests. Please try after sometime."),
frappe.RateLimitExceededError,
)
return fn(*args, **kwargs)

View file

@ -114,21 +114,9 @@ def emit_via_redis(event, message, room):
@frappe.whitelist(allow_guest=True)
def can_subscribe_doc(doctype: str, docname: str) -> bool:
from frappe.exceptions import PermissionError
if not frappe.has_permission(doctype=doctype, doc=docname, ptype="read"):
raise PermissionError()
return True
@frappe.whitelist(allow_guest=True)
def can_subscribe_doctype(doctype: str) -> bool:
from frappe.exceptions import PermissionError
if not frappe.has_permission(doctype=doctype, ptype="read"):
raise PermissionError()
def has_permission(doctype: str, name: str) -> bool:
if not frappe.has_permission(doctype=doctype, doc=name, ptype="read"):
raise frappe.PermissionError
return True
@ -138,6 +126,7 @@ def get_user_info():
return {
"user": frappe.session.user,
"user_type": frappe.session.data.user_type,
"installed_apps": frappe.get_installed_apps(),
}

View file

@ -82,8 +82,6 @@ def delete_session(sid=None, user=None, reason="Session Expired"):
# we should just ignore it till database is back up again.
return
frappe.cache.hdel("session", sid)
frappe.cache.hdel("last_db_session_update", sid)
if sid and not user:
table = frappe.qb.DocType("Sessions")
user_details = frappe.qb.from_(table).where(table.sid == sid).select(table.user).run(as_dict=True)
@ -94,6 +92,9 @@ def delete_session(sid=None, user=None, reason="Session Expired"):
frappe.db.delete("Sessions", {"sid": sid})
frappe.db.commit()
frappe.cache.hdel("session", sid)
frappe.cache.hdel("last_db_session_update", sid)
def clear_all_sessions(reason=None):
"""This effectively logs out all users"""
@ -167,6 +168,7 @@ def get():
bootinfo["desk_theme"] = frappe.db.get_value("User", frappe.session.user, "desk_theme") or "Light"
bootinfo["user"]["impersonated_by"] = frappe.session.data.get("impersonated_by")
bootinfo["navbar_settings"] = frappe.get_cached_doc("Navbar Settings")
return bootinfo
@ -358,7 +360,7 @@ class Session:
def update(self, force=False):
"""extend session expiry"""
if frappe.session["user"] == "Guest" or frappe.form_dict.cmd == "logout":
if frappe.session.user == "Guest":
return
now = frappe.utils.now()

View file

@ -152,7 +152,7 @@ class TestAuth(FrappeTestCase):
# Rate limiting
for _ in range(6):
res = requests.get(_generate_temporary_login_link(user, 10))
if res.status_code == 417:
if res.status_code == 429:
break
else:
self.fail("Rate limting not working")

View file

@ -144,7 +144,6 @@ class TestClient(FrappeTestCase):
first_item = data["message"][0]
self.assertTrue("name" in first_item)
self.assertTrue("modified" in first_item)
frappe.local.login_manager.logout()
def test_client_get(self):
from frappe.client import get

View file

@ -35,7 +35,7 @@ class TestDBUpdate(FrappeTestCase):
)
default = field_def.default if field_def.default is not None else fallback_default
self.assertEqual(fieldtype, table_column.type)
self.assertIn(fieldtype, table_column.type, msg=f"Types not matching for {fieldname}")
self.assertIn(cstr(table_column.default) or "NULL", [cstr(default), f"'{default}'"])
def test_index_and_unique_constraints(self):
@ -99,6 +99,16 @@ class TestDBUpdate(FrappeTestCase):
len(indexes), 1, msg=f"There should be 1 index on {doctype}.{field}, found {indexes}"
)
def test_bigint_conversion(self):
doctype = new_doctype(fields=[{"fieldname": "int_field", "fieldtype": "Int"}]).insert()
with self.assertRaises(frappe.CharacterLengthExceededError):
frappe.get_doc(doctype=doctype.name, int_field=2**62 - 1).insert()
doctype.fields[0].length = 14
doctype.save()
frappe.get_doc(doctype=doctype.name, int_field=2**62 - 1).insert()
@run_only_if(db_type_is.MARIADB)
def test_unique_index_on_install(self):
"""Only one unique index should be added"""
@ -157,7 +167,7 @@ class TestDBUpdate(FrappeTestCase):
def get_fieldtype_from_def(field_def):
fieldtuple = frappe.db.type_map.get(field_def.fieldtype, ("", 0))
fieldtype = fieldtuple[0]
if fieldtype in ("varchar", "datetime", "int"):
if fieldtype in ("varchar", "datetime"):
fieldtype += f"({field_def.length or fieldtuple[1]})"
return fieldtype

View file

@ -421,13 +421,6 @@ class TestPermissions(FrappeTestCase):
clear_user_permissions_for_doctype("Salutation")
clear_user_permissions_for_doctype("Contact")
def test_user_permissions_not_applied_if_user_can_edit_user_permissions(self):
add_user_permission("Blogger", "_Test Blogger 1", "test1@example.com")
# test1@example.com has rights to create user permissions
# so it should not matter if explicit user permissions are not set
self.assertTrue(frappe.get_doc("Blogger", "_Test Blogger").has_permission("read"))
def test_user_permission_is_not_applied_if_user_roles_does_not_have_permission(self):
add_user_permission("Blog Post", "-test-blog-post-1", "test3@example.com")
frappe.set_user("test3@example.com")

View file

@ -126,7 +126,7 @@ def capture_exception(message: str | None = None) -> None:
exc_info = sys.exc_info()
if any(exc_info):
# Don't report validation errors
if isinstance(exc_info[0], frappe.ValidationError):
if isinstance(exc_info[1], frappe.ValidationError):
return
event, hint = event_from_exception(

118
frappe/www/attribution.html Normal file
View file

@ -0,0 +1,118 @@
{% extends "templates/web.html" %}
{% block head_include %}
<link rel="stylesheet" href="/assets/frappe/css/fonts/fontawesome/font-awesome.min.css">
{% endblock %}
{% block page_content %}
<h1>{{ _("Attribution") }}</h1>
<p>
{{ _("This software is built on top of many open source packages. We would like to thank the authors of these
packages for their contribution.") }}
</p>
{% for app_info in apps %}
<section id="{{ app_info.name }}">
<h2><a href="#{{ app_info.name }}">{{ app_info.name }}</a></h2>
<table class="table">
<tr>
<th>{{ _("Authors") }}</th>
<td>{{ app_info.authors }}</td>
</tr>
<tr>
<th>{{ _("Description") }}</th>
<td>{{ app_info.description }}</td>
</tr>
<tr>
<th>{{ _("Dependencies") }}</th>
<td>
<table class="table table-striped">
<thead>
<tr>
<th>{{ _("Package") }}</th>
<th>{{ _("Type") }}</th>
<th>{{ _("License") }}</th>
<th>{{ _("Authors / Maintainers") }}</th>
</tr>
</thead>
<tbody>
{% for package in app_info["dependencies"] %}
<tr>
<td class="package">
{{ package.name | e }}
</td>
<td class="type">{{ package.type }}</td>
<td class="license"><i class="fa fa-spinner fa-spin"></i></td>
<td class="author"><i class="fa fa-spinner fa-spin"></i></td>
</tr>
{% endfor %}
</tbody>
</table>
</td>
</table>
</section>
{% endfor %}
<script>
const tables = document.querySelectorAll('table.table-striped');
tables.forEach((table) => {
var package_cells = table.querySelectorAll('td.package');
package_cells.forEach((package_cell) => {
var name = package_cell.innerText;
var type_cell = package_cell.nextElementSibling;
var license_cell = type_cell.nextElementSibling;
var author_cell = license_cell.nextElementSibling;
if (type_cell.innerText === "JavaScript") {
get_info_from_npm(name).then((info) => {
license_cell.innerText = info.license;
author_cell.innerText = info.author;
});
}
else if (type_cell.innerText === "Python") {
get_info_from_pypi(name).then((info) => {
license_cell.innerText = info.license;
author_cell.innerText = info.author;
});
}
});
})
function get_info_from_npm(package) {
const registry_url = `https://registry.npmjs.org/${package}`;
return fetch(registry_url)
.then((response) => response.json())
.then((data) => {
return {
license: (
typeof data.license === "object" ? data.license.type : data.license
) || "Unknown",
author: (
data.author?.name ||
data.maintainers?.map((c) => c.name).join(", ") ||
"Unknown"
)
}
});
}
function get_info_from_pypi(package) {
const registry_url = `https://pypi.org/pypi/${package}/json`;
return fetch(registry_url)
.then((response) => response.json())
.then((data) => {
return {
license: data.info.license || "Unknown",
author: (
data.info.author ||
data.info.maintainer ||
data.info.author_email ||
data.info.maintainer_email ||
"Unknown"
)
}
});
}
</script>
{% endblock %}

70
frappe/www/attribution.py Normal file
View file

@ -0,0 +1,70 @@
import json
import re
from pathlib import Path
import tomllib
import frappe
from frappe import _
from frappe.permissions import is_system_user
def get_context(context):
if not is_system_user():
frappe.throw(_("You need to be a system user to access this page."), frappe.PermissionError)
apps = []
for app in frappe.get_installed_apps():
app_info = get_app_info(app)
if any([app_info.get("authors"), app_info.get("dependencies"), app_info.get("description")]):
apps.append(app_info)
context.apps = apps
def get_app_info(app: str):
app_info = get_pyproject_info(app)
result = {
"name": app,
"description": app_info.get("description", ""),
"authors": ", ".join([a.get("name", "") for a in app_info.get("authors", [])]),
"dependencies": [],
}
for requirement in app_info.get("dependencies", []):
name = parse_pip_requirement(requirement)
result["dependencies"].append({"name": name, "type": "Python"})
result["dependencies"].extend(get_js_deps(app))
return result
def get_js_deps(app: str) -> list[dict]:
package_json = Path(frappe.get_app_path(app, "..", "package.json"))
if not package_json.exists():
return {}
with open(package_json) as f:
package = json.load(f)
packages = package.get("dependencies", {}).keys()
return [{"name": name, "type": "JavaScript"} for name in packages]
def get_pyproject_info(app: str) -> dict:
pyproject_toml = Path(frappe.get_app_path(app, "..", "pyproject.toml"))
if not pyproject_toml.exists():
return {}
with open(pyproject_toml, "rb") as f:
pyproject = tomllib.load(f)
return pyproject.get("project", {})
def parse_pip_requirement(requirement: str) -> str:
"""Parse pip requirement string to package name and version"""
match = re.match(r"^([A-Za-z0-9_\-\[\]]+)(.*)$", requirement)
return match[1] if match else requirement

View file

@ -73,7 +73,6 @@
"socket.io": "^4.7.1",
"socket.io-client": "^4.7.1",
"sortablejs": "^1.15.0",
"superagent": "^8.0.0",
"touch": "^3.1.0",
"vue": "^3.3.0",
"vue-router": "^4.1.5",

View file

@ -15,7 +15,7 @@ dependencies = [
"filetype~=1.2.0",
"GitPython~=3.1.34",
"Jinja2~=3.1.2",
"Pillow~=10.2.0",
"Pillow~=10.3.0",
"PyJWT~=2.8.0",
# We depend on internal attributes,
# do NOT add loose requirements on PyMySQL versions.

View file

@ -1,10 +1,7 @@
const { frappe_request } = require("../utils");
const log = console.log;
const WEBSITE_ROOM = "website";
const SITE_ROOM = "all";
function frappe_handlers(realtime, socket) {
function frappe_handlers(socket) {
socket.join(user_room(socket.user));
socket.join(WEBSITE_ROOM);
@ -12,13 +9,23 @@ function frappe_handlers(realtime, socket) {
socket.join(SITE_ROOM);
}
socket.has_permission = (doctype, name) => {
return new Promise((resolve) => {
socket
.frappe_request("/api/method/frappe.realtime.has_permission", { doctype, name })
.then((res) => res.json())
.then(({ message }) => {
if (message) {
resolve();
}
})
.catch((err) => console.log("Can't check permissions", err));
});
};
socket.on("doctype_subscribe", function (doctype) {
can_subscribe_doctype({
socket,
doctype,
callback: () => {
socket.join(doctype_room(doctype));
},
socket.has_permission(doctype).then(() => {
socket.join(doctype_room(doctype));
});
});
@ -42,14 +49,8 @@ function frappe_handlers(realtime, socket) {
});
socket.on("doc_subscribe", function (doctype, docname) {
can_subscribe_doc({
socket,
doctype,
docname,
callback: () => {
let room = doc_room(doctype, docname);
socket.join(room);
},
socket.has_permission(doctype, docname).then(() => {
socket.join(doc_room(doctype, docname));
});
});
@ -59,23 +60,18 @@ function frappe_handlers(realtime, socket) {
});
socket.on("doc_open", function (doctype, docname) {
can_subscribe_doc({
socket,
doctype,
docname,
callback: () => {
let room = open_doc_room(doctype, docname);
socket.join(room);
if (!socket.subscribed_documents) socket.subscribed_documents = [];
socket.subscribed_documents.push([doctype, docname]);
socket.has_permission(doctype, docname).then(() => {
let room = open_doc_room(doctype, docname);
socket.join(room);
if (!socket.subscribed_documents) socket.subscribed_documents = [];
socket.subscribed_documents.push([doctype, docname]);
// show who is currently viewing the form
notify_subscribed_doc_users({
socket: socket,
doctype: doctype,
docname: docname,
});
},
// show who is currently viewing the form
notify_subscribed_doc_users({
socket: socket,
doctype: doctype,
docname: docname,
});
});
});
@ -110,28 +106,6 @@ function notify_disconnected_documents(socket) {
}
}
function can_subscribe_doctype(args) {
if (!args) return;
if (!args.doctype) return;
frappe_request("/api/method/frappe.realtime.can_subscribe_doctype", args.socket)
.type("form")
.query({
doctype: args.doctype,
})
.end(function (err, res) {
if (!res || res.status == 403 || err) {
if (err) {
log(err);
}
return false;
} else if (res.status == 200) {
args.callback && args.callback(err, res);
return true;
}
log("ERROR (can_subscribe_doctype): ", err, res);
});
}
function notify_subscribed_doc_users(args) {
if (!(args && args.doctype && args.docname)) {
return;
@ -160,30 +134,6 @@ function notify_subscribed_doc_users(args) {
});
}
function can_subscribe_doc(args) {
if (!args) return;
if (!args.doctype || !args.docname) return;
frappe_request("/api/method/frappe.realtime.can_subscribe_doc", args.socket)
.type("form")
.query({
doctype: args.doctype,
docname: args.docname,
})
.end(function (err, res) {
if (!res) {
log("No response for doc_subscribe");
} else if (res.status == 403) {
return;
} else if (err) {
log(err);
} else if (res.status == 200) {
args.callback(err, res);
} else {
log("Something went wrong", err, res);
}
});
}
const doc_room = (doctype, docname) => "doc:" + doctype + "/" + docname;
const open_doc_room = (doctype, docname) => "open_doc:" + doctype + "/" + docname;
const user_room = (user) => "user:" + user;

View file

@ -1,6 +1,8 @@
const { Server } = require("socket.io");
const http = require("node:http");
const fs = require("fs");
const path = require("path");
const { get_conf, get_redis_subscriber } = require("../node_utils");
const conf = get_conf();
@ -25,10 +27,16 @@ const authenticate = require("./middlewares/authenticate");
realtime.use(authenticate);
// =======================
// load and register handlers
const frappe_handlers = require("./handlers/frappe_handlers");
function on_connection(socket) {
frappe_handlers(realtime, socket);
socket.installed_apps.forEach((app) => {
let app_handler = get_app_handlers(app);
try {
app_handler && app_handler(socket);
} catch (err) {
console.warn(`failed to setup event handlers from ${app}`);
console.warn(err);
}
});
// ESBUild "open in editor" on error
socket.on("open_in_editor", async (data) => {
@ -37,6 +45,27 @@ function on_connection(socket) {
});
}
const _app_handlers = {};
function get_app_handlers(app) {
if (app in _app_handlers) {
return _app_handlers[app];
}
let file = `../../${app}/realtime/handlers.js`;
let abs_path = path.resolve(__dirname, file);
let handler = null;
if (fs.existsSync(abs_path)) {
try {
handler = require(file);
} catch (err) {
console.warn(`failed to load event handlers from ${abs_path}`);
console.warn(err);
}
}
_app_handlers[app] = handler;
return handler;
}
realtime.on("connection", on_connection);
// =======================

View file

@ -1,8 +1,6 @@
const cookie = require("cookie");
const request = require("superagent");
const { get_url } = require("../utils");
const { get_conf } = require("../../node_utils");
const { get_url } = require("../utils");
const conf = get_conf();
function authenticate_with_frappe(socket, next) {
@ -30,21 +28,35 @@ function authenticate_with_frappe(socket, next) {
next(new Error("No authentication method used. Use cookie or authorization header."));
return;
}
socket.sid = cookies.sid;
socket.authorization_header = authorization_header;
let auth_req = request.get(get_url(socket, "/api/method/frappe.realtime.get_user_info"));
if (cookies.sid) {
auth_req = auth_req.query({ sid: cookies.sid });
} else {
auth_req = auth_req.set("Authorization", authorization_header);
}
socket.frappe_request = (path, args = {}, opts = {}) => {
let query_args = new URLSearchParams(args);
if (query_args.toString()) {
path = path + "?" + query_args.toString();
}
auth_req
.type("form")
.then((res) => {
socket.user = res.body.message.user;
socket.user_type = res.body.message.user_type;
socket.sid = cookies.sid;
socket.authorization_header = authorization_header;
let headers = {};
if (socket.sid) {
headers["Cookie"] = `sid=${socket.sid}`;
} else if (socket.authorization_header) {
headers["Authorization"] = socket.authorization_header;
}
return fetch(get_url(socket, path), {
...opts,
headers,
});
};
socket
.frappe_request("/api/method/frappe.realtime.get_user_info")
.then((res) => res.json())
.then(({ message }) => {
socket.user = message.user;
socket.user_type = message.user_type;
socket.installed_apps = message.installed_apps;
next();
})
.catch((e) => {

View file

@ -1,6 +1,5 @@
const { get_conf } = require("../node_utils");
const conf = get_conf();
const request = require("superagent");
function get_url(socket, path) {
if (!path) {
@ -17,17 +16,6 @@ function get_url(socket, path) {
return url + path;
}
// Authenticates a partial request created using superagent
function frappe_request(path, socket) {
const partial_req = request.get(get_url(socket, path));
if (socket.sid) {
return partial_req.query({ sid: socket.sid });
} else if (socket.authorization_header) {
return partial_req.set("Authorization", socket.authorization_header);
}
}
module.exports = {
get_url,
frappe_request,
};

121
yarn.lock
View file

@ -449,7 +449,7 @@ array-buffer-byte-length@^1.0.0:
call-bind "^1.0.2"
is-array-buffer "^3.0.1"
asap@^2.0.0, asap@~2.0.3:
asap@~2.0.3:
version "2.0.6"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==
@ -459,11 +459,6 @@ assert-never@^1.2.1:
resolved "https://registry.yarnpkg.com/assert-never/-/assert-never-1.2.1.tgz#11f0e363bf146205fb08193b5c7b90f4d1cf44fe"
integrity sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
at-least-node@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
@ -705,13 +700,6 @@ colord@^2.9.1:
resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43"
integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
commander@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
@ -722,11 +710,6 @@ commander@^9.0.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30"
integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==
component-emitter@^1.3.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17"
integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@ -752,11 +735,6 @@ cookie@^0.4.0, cookie@~0.4.1:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==
cookiejar@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b"
integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==
copy-anything@^2.0.1:
version "2.0.6"
resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.6.tgz#092454ea9584a7b7ad5573062b2a87f5900fc480"
@ -989,7 +967,7 @@ debug@^3.2.6:
dependencies:
ms "^2.1.1"
debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2:
debug@^4.3.2, debug@~4.3.1, debug@~4.3.2:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@ -1062,19 +1040,6 @@ define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0:
has-property-descriptors "^1.0.0"
object-keys "^1.1.1"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
dezalgo@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81"
integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==
dependencies:
asap "^2.0.0"
wrappy "1"
doctypes@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9"
@ -1408,11 +1373,6 @@ fast-glob@^3.2.5:
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-safe-stringify@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
fastparse@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9"
@ -1454,25 +1414,6 @@ for-each@^0.3.3:
dependencies:
is-callable "^1.1.3"
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
formidable@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.2.tgz#fa973a2bec150e4ce7cac15589d7a25fc30ebd89"
integrity sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==
dependencies:
dezalgo "^1.0.4"
hexoid "^1.0.0"
once "^1.4.0"
qs "^6.11.0"
fraction.js@^4.3.6:
version "4.3.7"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
@ -1673,11 +1614,6 @@ hasown@^2.0.0:
dependencies:
function-bind "^1.1.2"
hexoid@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18"
integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==
highlight.js@^10.4.1:
version "10.7.3"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
@ -2122,13 +2058,6 @@ lru-cache@^4.1.2:
pseudomap "^1.0.2"
yallist "^2.1.2"
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
dependencies:
yallist "^4.0.0"
magic-string@^0.30.5:
version "0.30.5"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9"
@ -2170,11 +2099,6 @@ merge2@^1.3.0:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
methods@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
micromatch@^4.0.4:
version "4.0.5"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
@ -2188,18 +2112,13 @@ mime-db@1.52.0:
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12, mime-types@~2.1.34:
mime-types@~2.1.34:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mime@2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367"
integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==
mime@^1.4.1:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
@ -2345,7 +2264,7 @@ object.assign@^4.1.4:
has-symbols "^1.0.3"
object-keys "^1.1.1"
once@^1.3.0, once@^1.4.0:
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
@ -2872,13 +2791,6 @@ pug@^3.0.1:
pug-runtime "^3.0.1"
pug-strip-comments "^2.0.0"
qs@^6.11.0:
version "6.11.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9"
integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==
dependencies:
side-channel "^1.0.4"
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
@ -3077,13 +2989,6 @@ semver@^6.3.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.3.8:
version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
dependencies:
lru-cache "^6.0.0"
set-function-length@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed"
@ -3293,22 +3198,6 @@ stylus@^0.x:
sax "~1.3.0"
source-map "^0.7.3"
superagent@^8.0.0:
version "8.1.2"
resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.1.2.tgz#03cb7da3ec8b32472c9d20f6c2a57c7f3765f30b"
integrity sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==
dependencies:
component-emitter "^1.3.0"
cookiejar "^2.1.4"
debug "^4.3.4"
fast-safe-stringify "^2.1.1"
form-data "^4.0.0"
formidable "^2.1.2"
methods "^1.1.2"
mime "2.6.0"
qs "^6.11.0"
semver "^7.3.8"
supports-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
@ -3569,7 +3458,7 @@ y18n@^5.0.5:
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
yallist@4.0.0, yallist@^4.0.0:
yallist@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==