');
+ $wrapper.append($doctype_select, $field_select);
+ field.$input_wrapper.append($wrapper);
+ $doctype_select.wrap('
');
+ $field_select.wrap('
');
+
+ let row = frappe.get_doc(doctype, docname);
+ let curr_value = { doctype: null, fieldname: null };
+ if (row.fetch_from) {
+ let [doctype, fieldname] = row.fetch_from.split(".");
+ curr_value.doctype = doctype;
+ curr_value.fieldname = fieldname;
+ }
+ let curr_df_link_doctype = row.fieldtype == "Link" ? row.options : null;
+
+ let doctypes = frm.doc.fields
+ .filter(df => df.fieldtype == "Link")
+ .filter(df => df.options && df.options != curr_df_link_doctype)
+ .map(df => ({
+ label: `${df.options} (${df.fieldname})`,
+ value: df.fieldname
+ }));
+ $doctype_select.add_options([
+ { label: __("Select DocType"), value: "", selected: true },
+ ...doctypes
+ ]);
+
+ $doctype_select.on("change", () => {
+ row.fetch_from = "";
+ frm.dirty();
+ update_fieldname_options();
+ });
+
+ function update_fieldname_options() {
+ $field_select.find("option").remove();
+
+ let link_fieldname = $doctype_select.val();
+ if (!link_fieldname) return;
+ let link_field = frm.doc.fields.find(
+ df => df.fieldname === link_fieldname
+ );
+ let link_doctype = link_field.options;
+ frappe.model.with_doctype(link_doctype, () => {
+ let fields = frappe.meta
+ .get_docfields(link_doctype, null, {
+ fieldtype: ["not in", frappe.model.no_value_type]
+ })
+ .map(df => ({
+ label: `${df.label} (${df.fieldtype})`,
+ value: df.fieldname
+ }));
+ $field_select.add_options([
+ {
+ label: __("Select Field"),
+ value: "",
+ selected: true,
+ disabled: true
+ },
+ ...fields
+ ]);
+
+ if (curr_value.fieldname) {
+ $field_select.val(curr_value.fieldname);
+ }
+ });
+ }
+
+ $field_select.on("change", () => {
+ let fetch_from = `${$doctype_select.val()}.${$field_select.val()}`;
+ row.fetch_from = fetch_from;
+ frm.dirty();
+ });
+
+ if (curr_value.doctype) {
+ $doctype_select.val(curr_value.doctype);
+ update_fieldname_options();
+ }
+ }
+});
diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json
index 7f93d3130a..e18edc1512 100644
--- a/frappe/core/doctype/doctype/doctype.json
+++ b/frappe/core/doctype/doctype/doctype.json
@@ -1,665 +1,686 @@
{
- "actions": [],
- "allow_rename": 1,
- "autoname": "Prompt",
- "creation": "2013-02-18 13:36:19",
- "description": "DocType is a Table / Form in the application.",
- "doctype": "DocType",
- "document_type": "Document",
- "engine": "InnoDB",
- "field_order": [
- "sb0",
- "module",
- "is_submittable",
- "istable",
- "issingle",
- "is_tree",
- "editable_grid",
- "quick_entry",
- "cb01",
- "track_changes",
- "track_seen",
- "track_views",
- "custom",
- "beta",
- "is_virtual",
- "fields_section_break",
- "fields",
- "sb1",
- "autoname",
- "name_case",
- "column_break_15",
- "description",
- "documentation",
- "form_settings_section",
- "image_field",
- "timeline_field",
- "nsm_parent_field",
- "max_attachments",
- "column_break_23",
- "hide_toolbar",
- "allow_copy",
- "allow_rename",
- "allow_import",
- "allow_events_in_timeline",
- "allow_auto_repeat",
- "view_settings",
- "title_field",
- "search_fields",
- "default_print_format",
- "sort_field",
- "sort_order",
- "column_break_29",
- "document_type",
- "icon",
- "color",
- "show_preview_popup",
- "show_name_in_global_search",
- "email_settings_sb",
- "default_email_template",
- "column_break_51",
- "email_append_to",
- "sender_field",
- "subject_field",
- "sb2",
- "permissions",
- "restrict_to_domain",
- "read_only",
- "in_create",
- "actions_section",
- "actions",
- "links_section",
- "links",
- "web_view",
- "has_web_view",
- "allow_guest_to_view",
- "index_web_pages_for_search",
- "route",
- "is_published_field",
- "advanced",
- "engine"
- ],
- "fields": [
- {
- "fieldname": "sb0",
- "fieldtype": "Section Break",
- "oldfieldtype": "Section Break"
- },
- {
- "fieldname": "module",
- "fieldtype": "Link",
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Module",
- "oldfieldname": "module",
- "oldfieldtype": "Link",
- "options": "Module Def",
- "reqd": 1,
- "search_index": 1
- },
- {
- "default": "0",
- "depends_on": "eval:!doc.istable",
- "description": "Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended.",
- "fieldname": "is_submittable",
- "fieldtype": "Check",
- "label": "Is Submittable"
- },
- {
- "default": "0",
- "description": "Child Tables are shown as a Grid in other DocTypes",
- "fieldname": "istable",
- "fieldtype": "Check",
- "in_standard_filter": 1,
- "label": "Is Child Table",
- "oldfieldname": "istable",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "depends_on": "eval:!doc.istable",
- "description": "Single Types have only one record no tables associated. Values are stored in tabSingles",
- "fieldname": "issingle",
- "fieldtype": "Check",
- "in_standard_filter": 1,
- "label": "Is Single",
- "oldfieldname": "issingle",
- "oldfieldtype": "Check",
- "set_only_once": 1
- },
- {
- "default": "1",
- "depends_on": "istable",
- "fieldname": "editable_grid",
- "fieldtype": "Check",
- "label": "Editable Grid"
- },
- {
- "default": "0",
- "depends_on": "eval:!doc.istable && !doc.issingle",
- "description": "Open a dialog with mandatory fields to create a new record quickly",
- "fieldname": "quick_entry",
- "fieldtype": "Check",
- "label": "Quick Entry"
- },
- {
- "fieldname": "cb01",
- "fieldtype": "Column Break"
- },
- {
- "default": "1",
- "depends_on": "eval:!doc.istable",
- "description": "If enabled, changes to the document are tracked and shown in timeline",
- "fieldname": "track_changes",
- "fieldtype": "Check",
- "label": "Track Changes"
- },
- {
- "default": "0",
- "depends_on": "eval:!doc.istable",
- "description": "If enabled, the document is marked as seen, the first time a user opens it",
- "fieldname": "track_seen",
- "fieldtype": "Check",
- "label": "Track Seen"
- },
- {
- "default": "0",
- "depends_on": "eval:!doc.istable",
- "description": "If enabled, document views are tracked, this can happen multiple times",
- "fieldname": "track_views",
- "fieldtype": "Check",
- "label": "Track Views"
- },
- {
- "default": "0",
- "fieldname": "custom",
- "fieldtype": "Check",
- "label": "Custom?"
- },
- {
- "default": "0",
- "fieldname": "beta",
- "fieldtype": "Check",
- "label": "Beta"
- },
- {
- "fieldname": "fields_section_break",
- "fieldtype": "Section Break",
- "label": "Fields",
- "oldfieldtype": "Section Break"
- },
- {
- "fieldname": "fields",
- "fieldtype": "Table",
- "label": "Fields",
- "oldfieldname": "fields",
- "oldfieldtype": "Table",
- "options": "DocField"
- },
- {
- "fieldname": "sb1",
- "fieldtype": "Section Break",
- "label": "Naming"
- },
- {
- "description": "Naming Options:\n
- field:[fieldname] - By Field
- naming_series: - By Naming Series (field called naming_series must be present
- Prompt - Prompt user for a name
- [series] - Series by prefix (separated by a dot); for example PRE.#####
\n- format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
",
- "fieldname": "autoname",
- "fieldtype": "Data",
- "label": "Auto Name",
- "oldfieldname": "autoname",
- "oldfieldtype": "Data"
- },
- {
- "fieldname": "name_case",
- "fieldtype": "Select",
- "label": "Name Case",
- "oldfieldname": "name_case",
- "oldfieldtype": "Select",
- "options": "\nTitle Case\nUPPER CASE"
- },
- {
- "fieldname": "column_break_15",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "description",
- "fieldtype": "Small Text",
- "label": "Description",
- "oldfieldname": "description",
- "oldfieldtype": "Text"
- },
- {
- "collapsible": 1,
- "fieldname": "form_settings_section",
- "fieldtype": "Section Break",
- "label": "Form Settings"
- },
- {
- "description": "Must be of type \"Attach Image\"",
- "fieldname": "image_field",
- "fieldtype": "Data",
- "label": "Image Field"
- },
- {
- "depends_on": "eval:!doc.istable",
- "description": "Comments and Communications will be associated with this linked document",
- "fieldname": "timeline_field",
- "fieldtype": "Data",
- "label": "Timeline Field"
- },
- {
- "fieldname": "max_attachments",
- "fieldtype": "Int",
- "label": "Max Attachments",
- "oldfieldname": "max_attachments",
- "oldfieldtype": "Int"
- },
- {
- "fieldname": "column_break_23",
- "fieldtype": "Column Break"
- },
- {
- "default": "0",
- "fieldname": "hide_toolbar",
- "fieldtype": "Check",
- "label": "Hide Sidebar and Menu",
- "oldfieldname": "hide_toolbar",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "allow_copy",
- "fieldtype": "Check",
- "label": "Hide Copy",
- "oldfieldname": "allow_copy",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "allow_rename",
- "fieldtype": "Check",
- "label": "Allow Rename",
- "oldfieldname": "allow_rename",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "allow_import",
- "fieldtype": "Check",
- "label": "Allow Import (via Data Import Tool)"
- },
- {
- "default": "0",
- "fieldname": "allow_events_in_timeline",
- "fieldtype": "Check",
- "label": "Allow events in timeline"
- },
- {
- "default": "0",
- "fieldname": "allow_auto_repeat",
- "fieldtype": "Check",
- "label": "Allow Auto Repeat"
- },
- {
- "collapsible": 1,
- "fieldname": "view_settings",
- "fieldtype": "Section Break",
- "label": "View Settings"
- },
- {
- "depends_on": "eval:!doc.istable",
- "fieldname": "title_field",
- "fieldtype": "Data",
- "label": "Title Field"
- },
- {
- "depends_on": "eval:!doc.istable",
- "fieldname": "search_fields",
- "fieldtype": "Data",
- "label": "Search Fields",
- "oldfieldname": "search_fields",
- "oldfieldtype": "Data"
- },
- {
- "fieldname": "default_print_format",
- "fieldtype": "Data",
- "label": "Default Print Format"
- },
- {
- "default": "modified",
- "depends_on": "eval:!doc.istable",
- "fieldname": "sort_field",
- "fieldtype": "Data",
- "label": "Default Sort Field"
- },
- {
- "default": "DESC",
- "depends_on": "eval:!doc.istable",
- "fieldname": "sort_order",
- "fieldtype": "Select",
- "label": "Default Sort Order",
- "options": "ASC\nDESC"
- },
- {
- "fieldname": "column_break_29",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "document_type",
- "fieldtype": "Select",
- "label": "Show in Module Section",
- "oldfieldname": "document_type",
- "oldfieldtype": "Select",
- "options": "\nDocument\nSetup\nSystem\nOther"
- },
- {
- "fieldname": "icon",
- "fieldtype": "Data",
- "label": "Icon"
- },
- {
- "fieldname": "color",
- "fieldtype": "Data",
- "label": "Color"
- },
- {
- "default": "0",
- "fieldname": "show_preview_popup",
- "fieldtype": "Check",
- "label": "Show Preview Popup"
- },
- {
- "default": "0",
- "fieldname": "show_name_in_global_search",
- "fieldtype": "Check",
- "label": "Make \"name\" searchable in Global Search"
- },
- {
- "depends_on": "eval:!doc.istable",
- "fieldname": "sb2",
- "fieldtype": "Section Break",
- "label": "Permission Rules"
- },
- {
- "fieldname": "permissions",
- "fieldtype": "Table",
- "label": "Permissions",
- "oldfieldname": "permissions",
- "oldfieldtype": "Table",
- "options": "DocPerm"
- },
- {
- "fieldname": "restrict_to_domain",
- "fieldtype": "Link",
- "label": "Restrict To Domain",
- "options": "Domain"
- },
- {
- "default": "0",
- "fieldname": "read_only",
- "fieldtype": "Check",
- "label": "User Cannot Search",
- "oldfieldname": "read_only",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "in_create",
- "fieldtype": "Check",
- "label": "User Cannot Create",
- "oldfieldname": "in_create",
- "oldfieldtype": "Check"
- },
- {
- "depends_on": "eval:doc.custom===0",
- "fieldname": "web_view",
- "fieldtype": "Section Break",
- "label": "Web View"
- },
- {
- "default": "0",
- "fieldname": "has_web_view",
- "fieldtype": "Check",
- "label": "Has Web View"
- },
- {
- "default": "0",
- "depends_on": "has_web_view",
- "fieldname": "allow_guest_to_view",
- "fieldtype": "Check",
- "label": "Allow Guest to View"
- },
- {
- "depends_on": "eval:!doc.istable",
- "fieldname": "route",
- "fieldtype": "Data",
- "label": "Route"
- },
- {
- "depends_on": "has_web_view",
- "fieldname": "is_published_field",
- "fieldtype": "Data",
- "label": "Is Published Field"
- },
- {
- "collapsible": 1,
- "fieldname": "advanced",
- "fieldtype": "Section Break",
- "hidden": 1,
- "label": "Advanced"
- },
- {
- "default": "InnoDB",
- "depends_on": "eval:!doc.issingle",
- "fieldname": "engine",
- "fieldtype": "Select",
- "label": "Database Engine",
- "options": "InnoDB\nMyISAM"
- },
- {
- "default": "0",
- "description": "Tree structures are implemented using Nested Set",
- "fieldname": "is_tree",
- "fieldtype": "Check",
- "label": "Is Tree"
- },
- {
- "depends_on": "is_tree",
- "fieldname": "nsm_parent_field",
- "fieldtype": "Data",
- "label": "Parent Field (Tree)"
- },
- {
- "description": "URL for documentation or help",
- "fieldname": "documentation",
- "fieldtype": "Data",
- "label": "Documentation Link"
- },
- {
- "collapsible": 1,
- "collapsible_depends_on": "actions",
- "fieldname": "actions_section",
- "fieldtype": "Section Break",
- "label": "Actions"
- },
- {
- "fieldname": "actions",
- "fieldtype": "Table",
- "label": "Actions",
- "options": "DocType Action"
- },
- {
- "collapsible": 1,
- "collapsible_depends_on": "links",
- "fieldname": "links_section",
- "fieldtype": "Section Break",
- "label": "Linked Documents"
- },
- {
- "fieldname": "links",
- "fieldtype": "Table",
- "label": "Links",
- "options": "DocType Link"
- },
- {
- "depends_on": "email_append_to",
- "fieldname": "subject_field",
- "fieldtype": "Data",
- "label": "Subject Field"
- },
- {
- "depends_on": "email_append_to",
- "fieldname": "sender_field",
- "fieldtype": "Data",
- "label": "Sender Field",
- "mandatory_depends_on": "email_append_to"
- },
- {
- "default": "0",
- "fieldname": "email_append_to",
- "fieldtype": "Check",
- "label": "Allow document creation via Email"
- },
- {
- "collapsible": 1,
- "fieldname": "email_settings_sb",
- "fieldtype": "Section Break",
- "label": "Email Settings"
- },
- {
- "default": "1",
- "fieldname": "index_web_pages_for_search",
- "fieldtype": "Check",
- "label": "Index Web Pages for Search"
- },
- {
- "default": "0",
- "fieldname": "is_virtual",
- "fieldtype": "Check",
- "label": "Is Virtual"
- },
- {
- "fieldname": "default_email_template",
- "fieldtype": "Link",
- "label": "Default Email Template",
- "options": "Email Template"
- },
- {
- "fieldname": "column_break_51",
- "fieldtype": "Column Break"
- }
- ],
- "icon": "fa fa-bolt",
- "idx": 6,
- "links": [
- {
- "group": "Views",
- "link_doctype": "Report",
- "link_fieldname": "ref_doctype"
- },
- {
- "group": "Workflow",
- "link_doctype": "Workflow",
- "link_fieldname": "document_type"
- },
- {
- "group": "Workflow",
- "link_doctype": "Notification",
- "link_fieldname": "document_type"
- },
- {
- "group": "Customization",
- "link_doctype": "Custom Field",
- "link_fieldname": "dt"
- },
- {
- "group": "Customization",
- "link_doctype": "Client Script",
- "link_fieldname": "dt"
- },
- {
- "group": "Customization",
- "link_doctype": "Server Script",
- "link_fieldname": "reference_doctype"
- },
- {
- "group": "Workflow",
- "link_doctype": "Webhook",
- "link_fieldname": "webhook_doctype"
- },
- {
- "group": "Views",
- "link_doctype": "Print Format",
- "link_fieldname": "doc_type"
- },
- {
- "group": "Views",
- "link_doctype": "Web Form",
- "link_fieldname": "doc_type"
- },
- {
- "group": "Views",
- "link_doctype": "Calendar View",
- "link_fieldname": "reference_doctype"
- },
- {
- "group": "Views",
- "link_doctype": "Kanban Board",
- "link_fieldname": "reference_doctype"
- },
- {
- "group": "Workflow",
- "link_doctype": "Onboarding Step",
- "link_fieldname": "reference_document"
- },
- {
- "group": "Rules",
- "link_doctype": "Auto Repeat",
- "link_fieldname": "reference_doctype"
- },
- {
- "group": "Rules",
- "link_doctype": "Assignment Rule",
- "link_fieldname": "document_type"
- },
- {
- "group": "Rules",
- "link_doctype": "Energy Point Rule",
- "link_fieldname": "reference_doctype"
- }
- ],
- "modified": "2021-04-16 12:26:41.031135",
- "modified_by": "Administrator",
- "module": "Core",
- "name": "DocType",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Administrator",
- "share": 1,
- "write": 1
- }
- ],
- "route": "doctype",
- "search_fields": "module",
- "show_name_in_global_search": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "Prompt",
+ "creation": "2013-02-18 13:36:19",
+ "description": "DocType is a Table / Form in the application.",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "engine": "InnoDB",
+ "field_order": [
+ "sb0",
+ "module",
+ "is_submittable",
+ "istable",
+ "issingle",
+ "is_tree",
+ "editable_grid",
+ "quick_entry",
+ "cb01",
+ "track_changes",
+ "track_seen",
+ "track_views",
+ "custom",
+ "beta",
+ "is_virtual",
+ "fields_section_break",
+ "fields",
+ "sb1",
+ "naming_rule",
+ "autoname",
+ "name_case",
+ "allow_rename",
+ "column_break_15",
+ "description",
+ "documentation",
+ "form_settings_section",
+ "image_field",
+ "timeline_field",
+ "nsm_parent_field",
+ "max_attachments",
+ "column_break_23",
+ "hide_toolbar",
+ "allow_copy",
+ "allow_import",
+ "allow_events_in_timeline",
+ "allow_auto_repeat",
+ "view_settings",
+ "title_field",
+ "search_fields",
+ "default_print_format",
+ "sort_field",
+ "sort_order",
+ "column_break_29",
+ "document_type",
+ "icon",
+ "color",
+ "show_preview_popup",
+ "show_name_in_global_search",
+ "email_settings_sb",
+ "default_email_template",
+ "column_break_51",
+ "email_append_to",
+ "sender_field",
+ "subject_field",
+ "sb2",
+ "permissions",
+ "restrict_to_domain",
+ "read_only",
+ "in_create",
+ "actions_section",
+ "actions",
+ "links_section",
+ "links",
+ "web_view",
+ "has_web_view",
+ "allow_guest_to_view",
+ "index_web_pages_for_search",
+ "route",
+ "is_published_field",
+ "website_search_field",
+ "advanced",
+ "engine",
+ "migration_hash"
+ ],
+ "fields": [
+ {
+ "fieldname": "sb0",
+ "fieldtype": "Section Break",
+ "oldfieldtype": "Section Break"
+ },
+ {
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Module",
+ "oldfieldname": "module",
+ "oldfieldtype": "Link",
+ "options": "Module Def",
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!doc.istable",
+ "description": "Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended.",
+ "fieldname": "is_submittable",
+ "fieldtype": "Check",
+ "label": "Is Submittable"
+ },
+ {
+ "default": "0",
+ "description": "Child Tables are shown as a Grid in other DocTypes",
+ "fieldname": "istable",
+ "fieldtype": "Check",
+ "in_standard_filter": 1,
+ "label": "Is Child Table",
+ "oldfieldname": "istable",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!doc.istable",
+ "description": "Single Types have only one record no tables associated. Values are stored in tabSingles",
+ "fieldname": "issingle",
+ "fieldtype": "Check",
+ "in_standard_filter": 1,
+ "label": "Is Single",
+ "oldfieldname": "issingle",
+ "oldfieldtype": "Check",
+ "set_only_once": 1
+ },
+ {
+ "default": "1",
+ "depends_on": "istable",
+ "fieldname": "editable_grid",
+ "fieldtype": "Check",
+ "label": "Editable Grid"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!doc.istable && !doc.issingle",
+ "description": "Open a dialog with mandatory fields to create a new record quickly",
+ "fieldname": "quick_entry",
+ "fieldtype": "Check",
+ "label": "Quick Entry"
+ },
+ {
+ "fieldname": "cb01",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!doc.istable",
+ "description": "If enabled, changes to the document are tracked and shown in timeline",
+ "fieldname": "track_changes",
+ "fieldtype": "Check",
+ "label": "Track Changes"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!doc.istable",
+ "description": "If enabled, the document is marked as seen, the first time a user opens it",
+ "fieldname": "track_seen",
+ "fieldtype": "Check",
+ "label": "Track Seen"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!doc.istable",
+ "description": "If enabled, document views are tracked, this can happen multiple times",
+ "fieldname": "track_views",
+ "fieldtype": "Check",
+ "label": "Track Views"
+ },
+ {
+ "default": "0",
+ "fieldname": "custom",
+ "fieldtype": "Check",
+ "label": "Custom?"
+ },
+ {
+ "default": "0",
+ "fieldname": "beta",
+ "fieldtype": "Check",
+ "label": "Beta"
+ },
+ {
+ "fieldname": "fields_section_break",
+ "fieldtype": "Section Break",
+ "label": "Fields",
+ "oldfieldtype": "Section Break"
+ },
+ {
+ "fieldname": "fields",
+ "fieldtype": "Table",
+ "label": "Fields",
+ "oldfieldname": "fields",
+ "oldfieldtype": "Table",
+ "options": "DocField"
+ },
+ {
+ "fieldname": "sb1",
+ "fieldtype": "Section Break",
+ "label": "Naming"
+ },
+ {
+ "description": "Naming Options:\n
- field:[fieldname] - By Field
- naming_series: - By Naming Series (field called naming_series must be present
- Prompt - Prompt user for a name
- [series] - Series by prefix (separated by a dot); for example PRE.#####
\n- format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
",
+ "fieldname": "autoname",
+ "fieldtype": "Data",
+ "label": "Auto Name",
+ "oldfieldname": "autoname",
+ "oldfieldtype": "Data"
+ },
+ {
+ "fieldname": "name_case",
+ "fieldtype": "Select",
+ "label": "Name Case",
+ "oldfieldname": "name_case",
+ "oldfieldtype": "Select",
+ "options": "\nTitle Case\nUPPER CASE"
+ },
+ {
+ "fieldname": "column_break_15",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "label": "Description",
+ "oldfieldname": "description",
+ "oldfieldtype": "Text"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "form_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Form Settings"
+ },
+ {
+ "description": "Must be of type \"Attach Image\"",
+ "fieldname": "image_field",
+ "fieldtype": "Data",
+ "label": "Image Field"
+ },
+ {
+ "depends_on": "eval:!doc.istable",
+ "description": "Comments and Communications will be associated with this linked document",
+ "fieldname": "timeline_field",
+ "fieldtype": "Data",
+ "label": "Timeline Field"
+ },
+ {
+ "fieldname": "max_attachments",
+ "fieldtype": "Int",
+ "label": "Max Attachments",
+ "oldfieldname": "max_attachments",
+ "oldfieldtype": "Int"
+ },
+ {
+ "fieldname": "column_break_23",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "hide_toolbar",
+ "fieldtype": "Check",
+ "label": "Hide Sidebar and Menu",
+ "oldfieldname": "hide_toolbar",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_copy",
+ "fieldtype": "Check",
+ "label": "Hide Copy",
+ "oldfieldname": "allow_copy",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "1",
+ "fieldname": "allow_rename",
+ "fieldtype": "Check",
+ "label": "Allow Rename",
+ "oldfieldname": "allow_rename",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_import",
+ "fieldtype": "Check",
+ "label": "Allow Import (via Data Import Tool)"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_events_in_timeline",
+ "fieldtype": "Check",
+ "label": "Allow events in timeline"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_auto_repeat",
+ "fieldtype": "Check",
+ "label": "Allow Auto Repeat"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "view_settings",
+ "fieldtype": "Section Break",
+ "label": "View Settings"
+ },
+ {
+ "depends_on": "eval:!doc.istable",
+ "fieldname": "title_field",
+ "fieldtype": "Data",
+ "label": "Title Field"
+ },
+ {
+ "depends_on": "eval:!doc.istable",
+ "fieldname": "search_fields",
+ "fieldtype": "Data",
+ "label": "Search Fields",
+ "oldfieldname": "search_fields",
+ "oldfieldtype": "Data"
+ },
+ {
+ "fieldname": "default_print_format",
+ "fieldtype": "Data",
+ "label": "Default Print Format"
+ },
+ {
+ "default": "modified",
+ "depends_on": "eval:!doc.istable",
+ "fieldname": "sort_field",
+ "fieldtype": "Data",
+ "label": "Default Sort Field"
+ },
+ {
+ "default": "DESC",
+ "depends_on": "eval:!doc.istable",
+ "fieldname": "sort_order",
+ "fieldtype": "Select",
+ "label": "Default Sort Order",
+ "options": "ASC\nDESC"
+ },
+ {
+ "fieldname": "column_break_29",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "document_type",
+ "fieldtype": "Select",
+ "label": "Show in Module Section",
+ "oldfieldname": "document_type",
+ "oldfieldtype": "Select",
+ "options": "\nDocument\nSetup\nSystem\nOther"
+ },
+ {
+ "fieldname": "icon",
+ "fieldtype": "Data",
+ "label": "Icon"
+ },
+ {
+ "fieldname": "color",
+ "fieldtype": "Data",
+ "label": "Color"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_preview_popup",
+ "fieldtype": "Check",
+ "label": "Show Preview Popup"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_name_in_global_search",
+ "fieldtype": "Check",
+ "label": "Make \"name\" searchable in Global Search"
+ },
+ {
+ "depends_on": "eval:!doc.istable",
+ "fieldname": "sb2",
+ "fieldtype": "Section Break",
+ "label": "Permission Rules"
+ },
+ {
+ "fieldname": "permissions",
+ "fieldtype": "Table",
+ "label": "Permissions",
+ "oldfieldname": "permissions",
+ "oldfieldtype": "Table",
+ "options": "DocPerm"
+ },
+ {
+ "fieldname": "restrict_to_domain",
+ "fieldtype": "Link",
+ "label": "Restrict To Domain",
+ "options": "Domain"
+ },
+ {
+ "default": "0",
+ "fieldname": "read_only",
+ "fieldtype": "Check",
+ "label": "User Cannot Search",
+ "oldfieldname": "read_only",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "in_create",
+ "fieldtype": "Check",
+ "label": "User Cannot Create",
+ "oldfieldname": "in_create",
+ "oldfieldtype": "Check"
+ },
+ {
+ "depends_on": "eval:doc.custom===0",
+ "fieldname": "web_view",
+ "fieldtype": "Section Break",
+ "label": "Web View"
+ },
+ {
+ "default": "0",
+ "fieldname": "has_web_view",
+ "fieldtype": "Check",
+ "label": "Has Web View"
+ },
+ {
+ "default": "0",
+ "depends_on": "has_web_view",
+ "fieldname": "allow_guest_to_view",
+ "fieldtype": "Check",
+ "label": "Allow Guest to View"
+ },
+ {
+ "depends_on": "eval:!doc.istable",
+ "fieldname": "route",
+ "fieldtype": "Data",
+ "label": "Route"
+ },
+ {
+ "depends_on": "has_web_view",
+ "fieldname": "is_published_field",
+ "fieldtype": "Data",
+ "label": "Is Published Field"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "advanced",
+ "fieldtype": "Section Break",
+ "hidden": 1,
+ "label": "Advanced"
+ },
+ {
+ "default": "InnoDB",
+ "depends_on": "eval:!doc.issingle",
+ "fieldname": "engine",
+ "fieldtype": "Select",
+ "label": "Database Engine",
+ "options": "InnoDB\nMyISAM"
+ },
+ {
+ "default": "0",
+ "description": "Tree structures are implemented using Nested Set",
+ "fieldname": "is_tree",
+ "fieldtype": "Check",
+ "label": "Is Tree"
+ },
+ {
+ "depends_on": "is_tree",
+ "fieldname": "nsm_parent_field",
+ "fieldtype": "Data",
+ "label": "Parent Field (Tree)"
+ },
+ {
+ "description": "URL for documentation or help",
+ "fieldname": "documentation",
+ "fieldtype": "Data",
+ "label": "Documentation Link"
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "actions",
+ "fieldname": "actions_section",
+ "fieldtype": "Section Break",
+ "label": "Actions"
+ },
+ {
+ "fieldname": "actions",
+ "fieldtype": "Table",
+ "label": "Actions",
+ "options": "DocType Action"
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "links",
+ "fieldname": "links_section",
+ "fieldtype": "Section Break",
+ "label": "Linked Documents"
+ },
+ {
+ "fieldname": "links",
+ "fieldtype": "Table",
+ "label": "Links",
+ "options": "DocType Link"
+ },
+ {
+ "depends_on": "email_append_to",
+ "fieldname": "subject_field",
+ "fieldtype": "Data",
+ "label": "Subject Field"
+ },
+ {
+ "depends_on": "email_append_to",
+ "fieldname": "sender_field",
+ "fieldtype": "Data",
+ "label": "Sender Field",
+ "mandatory_depends_on": "email_append_to"
+ },
+ {
+ "default": "0",
+ "fieldname": "email_append_to",
+ "fieldtype": "Check",
+ "label": "Allow document creation via Email"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "email_settings_sb",
+ "fieldtype": "Section Break",
+ "label": "Email Settings"
+ },
+ {
+ "default": "1",
+ "fieldname": "index_web_pages_for_search",
+ "fieldtype": "Check",
+ "label": "Index Web Pages for Search"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_virtual",
+ "fieldtype": "Check",
+ "label": "Is Virtual"
+ },
+ {
+ "fieldname": "default_email_template",
+ "fieldtype": "Link",
+ "label": "Default Email Template",
+ "options": "Email Template"
+ },
+ {
+ "fieldname": "column_break_51",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "has_web_view",
+ "fieldname": "website_search_field",
+ "fieldtype": "Data",
+ "label": "Website Search Field"
+ },
+ {
+ "fieldname": "naming_rule",
+ "fieldtype": "Select",
+ "label": "Naming Rule",
+ "length": 40,
+ "options": "\nSet by user\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script"
+ },
+ {
+ "fieldname": "migration_hash",
+ "fieldtype": "Data",
+ "hidden": 1
+ }
+ ],
+ "icon": "fa fa-bolt",
+ "idx": 6,
+ "links": [
+ {
+ "group": "Views",
+ "link_doctype": "Report",
+ "link_fieldname": "ref_doctype"
+ },
+ {
+ "group": "Workflow",
+ "link_doctype": "Workflow",
+ "link_fieldname": "document_type"
+ },
+ {
+ "group": "Workflow",
+ "link_doctype": "Notification",
+ "link_fieldname": "document_type"
+ },
+ {
+ "group": "Customization",
+ "link_doctype": "Custom Field",
+ "link_fieldname": "dt"
+ },
+ {
+ "group": "Customization",
+ "link_doctype": "Client Script",
+ "link_fieldname": "dt"
+ },
+ {
+ "group": "Customization",
+ "link_doctype": "Server Script",
+ "link_fieldname": "reference_doctype"
+ },
+ {
+ "group": "Workflow",
+ "link_doctype": "Webhook",
+ "link_fieldname": "webhook_doctype"
+ },
+ {
+ "group": "Views",
+ "link_doctype": "Print Format",
+ "link_fieldname": "doc_type"
+ },
+ {
+ "group": "Views",
+ "link_doctype": "Web Form",
+ "link_fieldname": "doc_type"
+ },
+ {
+ "group": "Views",
+ "link_doctype": "Calendar View",
+ "link_fieldname": "reference_doctype"
+ },
+ {
+ "group": "Views",
+ "link_doctype": "Kanban Board",
+ "link_fieldname": "reference_doctype"
+ },
+ {
+ "group": "Workflow",
+ "link_doctype": "Onboarding Step",
+ "link_fieldname": "reference_document"
+ },
+ {
+ "group": "Rules",
+ "link_doctype": "Auto Repeat",
+ "link_fieldname": "reference_doctype"
+ },
+ {
+ "group": "Rules",
+ "link_doctype": "Assignment Rule",
+ "link_fieldname": "document_type"
+ },
+ {
+ "group": "Rules",
+ "link_doctype": "Energy Point Rule",
+ "link_fieldname": "reference_doctype"
+ }
+ ],
+ "modified": "2021-10-29 11:39:13.233403",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "DocType",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Administrator",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "route": "doctype",
+ "search_fields": "module",
+ "show_name_in_global_search": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index f801629329..738fb73a34 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
# imports - standard imports
import re, copy, os, shutil
@@ -8,7 +8,6 @@ from frappe.cache_manager import clear_user_cache, clear_controller_cache
# imports - module imports
import frappe
-import frappe.website.render
from frappe import _
from frappe.utils import now, cint
from frappe.model import no_value_fields, default_fields, data_fieldtypes, table_fields, data_field_options
@@ -23,6 +22,8 @@ from frappe.model.docfield import supports_translation
from frappe.modules.import_file import get_file_path
from frappe.model.meta import Meta
from frappe.desk.utils import validate_route_conflict
+from frappe.website.utils import clear_cache
+from frappe.query_builder.functions import Concat
class InvalidFieldNameError(frappe.ValidationError): pass
class UniqueFieldnameError(frappe.ValidationError): pass
@@ -86,10 +87,6 @@ class DocType(Document):
if self.default_print_format and not self.custom:
frappe.throw(_('Standard DocType cannot have default print format, use Customize Form'))
- if frappe.conf.get('developer_mode'):
- self.owner = 'Administrator'
- self.modified_by = 'Administrator'
-
def validate_field_name_conflicts(self):
"""Check if field names dont conflict with controller properties and methods"""
core_doctypes = [
@@ -176,7 +173,6 @@ class DocType(Document):
if self.is_virtual and self.custom:
frappe.throw(_("Not allowed to create custom Virtual DocType."), CannotCreateStandardDoctypeError)
-
if frappe.conf.get('developer_mode'):
self.owner = 'Administrator'
self.modified_by = 'Administrator'
@@ -248,7 +244,7 @@ class DocType(Document):
frappe.throw(_('Field "route" is mandatory for Web Views'), title='Missing Field')
# clear website cache
- frappe.website.render.clear_cache()
+ clear_cache()
def change_modified_of_parent(self):
"""Change the timestamp of parent DocType if the current one is a child to clear caches."""
@@ -274,6 +270,8 @@ class DocType(Document):
d.fieldname = d.fieldname + '_section'
elif d.fieldtype=='Column Break':
d.fieldname = d.fieldname + '_column'
+ elif d.fieldtype=='Tab Break':
+ d.fieldname = d.fieldname + '_tab'
else:
d.fieldname = d.fieldtype.lower().replace(" ","_") + "_" + str(d.idx)
else:
@@ -312,9 +310,7 @@ class DocType(Document):
if allow_doctype_export:
self.export_doc()
self.make_controller_template()
-
- if self.has_web_view:
- self.set_base_class_for_controller()
+ self.set_base_class_for_controller()
# update index
if not self.custom:
@@ -352,23 +348,49 @@ class DocType(Document):
now=now, doctype=self.name)
def set_base_class_for_controller(self):
- '''Updates the controller class to subclass from `WebsiteGenertor`,
- if it is a subclass of `Document`'''
- controller_path = frappe.get_module_path(frappe.scrub(self.module),
- 'doctype', frappe.scrub(self.name), frappe.scrub(self.name) + '.py')
+ """If DocType.has_web_view has been changed, updates the controller class and import
+ from `WebsiteGenertor` to `Document` or viceversa"""
- with open(controller_path, 'r') as f:
+ if not self.has_value_changed("has_web_view"):
+ return
+
+ despaced_name = self.name.replace(" ", "_")
+ scrubbed_name = frappe.scrub(self.name)
+ scrubbed_module = frappe.scrub(self.module)
+ controller_path = frappe.get_module_path(
+ scrubbed_module, "doctype", scrubbed_name, f"{scrubbed_name}.py"
+ )
+
+ document_cls_tag = f"class {despaced_name}(Document)"
+ document_import_tag = "from frappe.model.document import Document"
+ website_generator_cls_tag = f"class {despaced_name}(WebsiteGenerator)"
+ website_generator_import_tag = "from frappe.website.generators.website_generator import WebsiteGenerator"
+
+ with open(controller_path) as f:
code = f.read()
+ updated_code = code
- class_string = '\nclass {0}(Document)'.format(self.name.replace(' ', ''))
- if '\nfrom frappe.model.document import Document' in code and class_string in code:
- code = code.replace('from frappe.model.document import Document',
- 'from frappe.website.website_generator import WebsiteGenerator')
- code = code.replace('class {0}(Document)'.format(self.name.replace(' ', '')),
- 'class {0}(WebsiteGenerator)'.format(self.name.replace(' ', '')))
+ is_website_generator_class = all([
+ website_generator_cls_tag in code,
+ website_generator_import_tag in code
+ ])
- with open(controller_path, 'w') as f:
- f.write(code)
+ if self.has_web_view and not is_website_generator_class:
+ updated_code = updated_code.replace(
+ document_import_tag, website_generator_import_tag
+ ).replace(
+ document_cls_tag, website_generator_cls_tag
+ )
+ elif not self.has_web_view and is_website_generator_class:
+ updated_code = updated_code.replace(
+ website_generator_import_tag, document_import_tag
+ ).replace(
+ website_generator_cls_tag, document_cls_tag
+ )
+
+ if updated_code != code:
+ with open(controller_path, "w") as f:
+ f.write(updated_code)
def run_module_method(self, method):
from frappe.modules import load_doctype_module
@@ -396,10 +418,7 @@ class DocType(Document):
frappe.db.sql("""update tabSingles set value=%s
where doctype=%s and field='name' and value = %s""", (new, new, old))
else:
- frappe.db.multisql({
- "mariadb": f"RENAME TABLE `tab{old}` TO `tab{new}`",
- "postgres": f"ALTER TABLE `tab{old}` RENAME TO `tab{new}`"
- })
+ frappe.db.rename_table(old, new)
frappe.db.commit()
# Do not rename and move files and folders for custom doctype
@@ -466,7 +485,7 @@ class DocType(Document):
return
# check if atleast 1 record exists
- if not (frappe.db.table_exists(self.name) and frappe.db.sql("select name from `tab{}` limit 1".format(self.name))):
+ if not (frappe.db.table_exists(self.name) and frappe.get_all(self.name, fields=["name"], limit=1, as_list=True)):
return
existing_property_setter = frappe.db.get_value("Property Setter", {"doc_type": self.name,
@@ -496,6 +515,9 @@ class DocType(Document):
# retain order of 'fields' table and change order in 'field_order'
docdict["field_order"] = [f.fieldname for f in self.fields]
+ if self.custom:
+ return
+
path = get_file_path(self.module, "DocType", self.name)
if os.path.exists(path):
try:
@@ -550,11 +572,6 @@ class DocType(Document):
from frappe.modules.export_file import export_to_files
export_to_files(record_list=[['DocType', self.name]], create_init=True)
- def import_doc(self):
- """Import from standard folder `[module]/doctype/[name]/[name].json`."""
- from frappe.modules.import_module import import_from_files
- import_from_files(record_list=[[self.module, 'doctype', self.name]])
-
def make_controller_template(self):
"""Make boilerplate controller template."""
make_boilerplate("controller._py", self)
@@ -574,17 +591,17 @@ class DocType(Document):
def make_amendable(self):
"""If is_submittable is set, add amended_from docfields."""
if self.is_submittable:
- if not frappe.db.sql("""select name from tabDocField
- where fieldname = 'amended_from' and parent = %s""", self.name):
- self.append("fields", {
- "label": "Amended From",
- "fieldtype": "Link",
- "fieldname": "amended_from",
- "options": self.name,
- "read_only": 1,
- "print_hide": 1,
- "no_copy": 1
- })
+ docfield_exists = frappe.get_all("DocField", filters={"fieldname": "amended_from", "parent": self.name}, pluck="name", limit=1)
+ if not docfield_exists:
+ self.append("fields", {
+ "label": "Amended From",
+ "fieldtype": "Link",
+ "fieldname": "amended_from",
+ "options": self.name,
+ "read_only": 1,
+ "print_hide": 1,
+ "no_copy": 1
+ })
def make_repeatable(self):
"""If allow_auto_repeat is set, add auto_repeat custom field."""
@@ -709,12 +726,13 @@ def validate_series(dt, autoname=None, name=None):
and (not autoname.startswith('format:')):
prefix = autoname.split('.')[0]
- used_in = frappe.db.sql("""
- SELECT `name`
- FROM `tabDocType`
- WHERE `autoname` LIKE CONCAT(%s, '.%%')
- AND `name`!=%s
- """, (prefix, name))
+ doctype = frappe.qb.DocType("DocType")
+ used_in = (frappe.qb
+ .from_(doctype)
+ .select(doctype.name)
+ .where(doctype.autoname.like(Concat(prefix,".%")))
+ .where(doctype.name != name)
+ ).run()
if used_in:
frappe.throw(_("Series {0} already used in {1}").format(prefix, used_in[0][0]))
@@ -727,9 +745,22 @@ def validate_links_table_fieldnames(meta):
for index, link in enumerate(meta.links):
link_meta = frappe.get_meta(link.link_doctype)
if not link_meta.get_field(link.link_fieldname):
- message = _("Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype))
+ message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype))
frappe.throw(message, InvalidFieldNameError, _("Invalid Fieldname"))
+ if link.is_child_table and not meta.get_field(link.table_fieldname):
+ message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.table_fieldname), frappe.bold(meta.name))
+ frappe.throw(message, frappe.ValidationError, _("Invalid Table Fieldname"))
+
+ if link.is_child_table:
+ if not link.parent_doctype:
+ message = _("Document Links Row #{0}: Parent DocType is mandatory for internal links").format(index+1)
+ frappe.throw(message, frappe.ValidationError, _("Parent Missing"))
+
+ if not link.table_fieldname:
+ message = _("Document Links Row #{0}: Table Fieldname is mandatory for internal links").format(index+1)
+ frappe.throw(message, frappe.ValidationError, _("Table Fieldname Missing"))
+
def validate_fields_for_doctype(doctype):
meta = frappe.get_meta(doctype, cached=False)
validate_links_table_fieldnames(meta)
@@ -932,6 +963,16 @@ def validate_fields(meta):
if meta.is_published_field not in fieldname_list:
frappe.throw(_("Is Published Field must be a valid fieldname"), InvalidFieldNameError)
+ def check_website_search_field(meta):
+ if not meta.website_search_field:
+ return
+
+ if meta.website_search_field not in fieldname_list:
+ frappe.throw(_("Website Search Field must be a valid fieldname"), InvalidFieldNameError)
+
+ if "title" not in fieldname_list:
+ frappe.throw(_('Field "title" is mandatory if "Website Search Field" is set.'), title=_("Missing Field"))
+
def check_timeline_field(meta):
if not meta.timeline_field:
return
@@ -1012,6 +1053,9 @@ def validate_fields(meta):
frappe.throw(_('Option {0} for field {1} is not a child table')
.format(frappe.bold(doctype), frappe.bold(docfield.fieldname)), title=_("Invalid Option"))
+ def check_max_height(docfield):
+ if getattr(docfield, 'max_height', None) and (docfield.max_height[-2:] not in ('px', 'em')):
+ frappe.throw('Max for {} height must be in px, em, rem'.format(frappe.bold(docfield.fieldname)))
fields = meta.get("fields")
fieldname_list = [d.fieldname for d in fields]
@@ -1045,12 +1089,14 @@ def validate_fields(meta):
scrub_options_in_select(d)
scrub_fetch_from(d)
validate_data_field_type(d)
+ check_max_height(d)
check_fold(fields)
check_search_fields(meta, fields)
check_title_field(meta)
check_timeline_field(meta)
check_is_published_field(meta)
+ check_website_search_field(meta)
check_sort_field(meta)
check_image_field(meta)
@@ -1200,8 +1246,14 @@ def make_module_and_roles(doc, perm_fieldname="permissions"):
if ("tabModule Def" in frappe.db.get_tables()
and not frappe.db.exists("Module Def", doc.module)):
m = frappe.get_doc({"doctype": "Module Def", "module_name": doc.module})
- m.app_name = frappe.local.module_app[frappe.scrub(doc.module)]
+ if frappe.scrub(doc.module) in frappe.local.module_app:
+ m.app_name = frappe.local.module_app[frappe.scrub(doc.module)]
+ else:
+ m.app_name = 'frappe'
m.flags.ignore_mandatory = m.flags.ignore_permissions = True
+ if frappe.flags.package:
+ m.package = frappe.flags.package.name
+ m.custom = 1
m.insert()
default_roles = ["Administrator", "Guest", "All"]
diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py
index 1e1a01a685..4362a52c34 100644
--- a/frappe/core/doctype/doctype/test_doctype.py
+++ b/frappe/core/doctype/doctype/test_doctype.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
from frappe.core.doctype.doctype.doctype import (UniqueFieldnameError,
@@ -348,6 +348,7 @@ class TestDocType(unittest.TestCase):
dump_docs = json.dumps(docs.get('docs'))
cancel_all_linked_docs(dump_docs)
data_link_doc.cancel()
+ data_doc.name = '{}-CANC-0'.format(data_doc.name)
data_doc.load_from_db()
self.assertEqual(data_link_doc.docstatus, 2)
self.assertEqual(data_doc.docstatus, 2)
@@ -371,7 +372,7 @@ class TestDocType(unittest.TestCase):
for data in link_doc.get('permissions'):
data.submit = 1
data.cancel = 1
- link_doc.insert()
+ link_doc.insert(ignore_if_duplicate=True)
#create first parent doctype
test_doc_1 = new_doctype('Test Doctype 1')
@@ -386,7 +387,7 @@ class TestDocType(unittest.TestCase):
for data in test_doc_1.get('permissions'):
data.submit = 1
data.cancel = 1
- test_doc_1.insert()
+ test_doc_1.insert(ignore_if_duplicate=True)
#crete second parent doctype
doc = new_doctype('Test Doctype 2')
@@ -401,7 +402,7 @@ class TestDocType(unittest.TestCase):
for data in link_doc.get('permissions'):
data.submit = 1
data.cancel = 1
- doc.insert()
+ doc.insert(ignore_if_duplicate=True)
# create doctype data
data_link_doc_1 = frappe.new_doc('Test Linked Doctype 1')
@@ -432,6 +433,7 @@ class TestDocType(unittest.TestCase):
# checking that doc for Test Doctype 2 is not canceled
self.assertRaises(frappe.LinkExistsError, data_link_doc_1.cancel)
+ data_doc_2.name = '{}-CANC-0'.format(data_doc_2.name)
data_doc.load_from_db()
data_doc_2.load_from_db()
self.assertEqual(data_link_doc_1.docstatus, 2)
diff --git a/frappe/core/doctype/doctype_action/doctype_action.py b/frappe/core/doctype/doctype_action/doctype_action.py
index 203b06ec1b..807d1bf0b1 100644
--- a/frappe/core/doctype/doctype_action/doctype_action.py
+++ b/frappe/core/doctype/doctype_action/doctype_action.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/doctype_link/doctype_link.json b/frappe/core/doctype/doctype_link/doctype_link.json
index 0453894467..4baec6746d 100644
--- a/frappe/core/doctype/doctype_link/doctype_link.json
+++ b/frappe/core/doctype/doctype_link/doctype_link.json
@@ -7,8 +7,11 @@
"field_order": [
"link_doctype",
"link_fieldname",
+ "parent_doctype",
+ "table_fieldname",
"group",
"hidden",
+ "is_child_table",
"custom"
],
"fields": [
@@ -45,12 +48,33 @@
"fieldtype": "Check",
"hidden": 1,
"label": "Custom"
+ },
+ {
+ "depends_on": "is_child_table",
+ "fieldname": "parent_doctype",
+ "fieldtype": "Link",
+ "label": "Parent DocType",
+ "mandatory_depends_on": "is_child_table",
+ "options": "DocType"
+ },
+ {
+ "default": "0",
+ "fetch_from": "link_doctype.istable",
+ "fieldname": "is_child_table",
+ "fieldtype": "Check",
+ "label": "Is Child Table",
+ "read_only": 1
+ },
+ {
+ "fieldname": "table_fieldname",
+ "fieldtype": "Data",
+ "label": "Table Fieldname"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-09-24 14:19:25.189511",
+ "modified": "2021-07-31 15:23:12.237491",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType Link",
diff --git a/frappe/core/doctype/doctype_link/doctype_link.py b/frappe/core/doctype/doctype_link/doctype_link.py
index 07e0efdace..ca2c4efa16 100644
--- a/frappe/core/doctype/doctype_link/doctype_link.py
+++ b/frappe/core/doctype/doctype_link/doctype_link.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.json b/frappe/core/doctype/document_naming_rule/document_naming_rule.json
index 4a88e3be6e..4e6f3f3fd1 100644
--- a/frappe/core/doctype/document_naming_rule/document_naming_rule.json
+++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.json
@@ -41,6 +41,7 @@
"fieldname": "counter",
"fieldtype": "Int",
"label": "Counter",
+ "no_copy": 1,
"read_only": 1
},
{
@@ -79,7 +80,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-11-04 14:38:14.836056",
+ "modified": "2021-09-13 20:07:47.617615",
"modified_by": "Administrator",
"module": "Core",
"name": "Document Naming Rule",
diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.py b/frappe/core/doctype/document_naming_rule/document_naming_rule.py
index 10099bd19a..8013f9df6f 100644
--- a/frappe/core/doctype/document_naming_rule/document_naming_rule.py
+++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/document_naming_rule/test_document_naming_rule.py b/frappe/core/doctype/document_naming_rule/test_document_naming_rule.py
index 2206d173d7..50f1386758 100644
--- a/frappe/core/doctype/document_naming_rule/test_document_naming_rule.py
+++ b/frappe/core/doctype/document_naming_rule/test_document_naming_rule.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py b/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py
index dfca052d95..4706492cea 100644
--- a/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py
+++ b/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/document_naming_rule_condition/test_document_naming_rule_condition.py b/frappe/core/doctype/document_naming_rule_condition/test_document_naming_rule_condition.py
index 643e963bd7..3d0565234c 100644
--- a/frappe/core/doctype/document_naming_rule_condition/test_document_naming_rule_condition.py
+++ b/frappe/core/doctype/document_naming_rule_condition/test_document_naming_rule_condition.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/core/doctype/domain/domain.py b/frappe/core/doctype/domain/domain.py
index bbd20f3b70..ebd6e3ac9e 100644
--- a/frappe/core/doctype/domain/domain.py
+++ b/frappe/core/doctype/domain/domain.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/core/doctype/domain/test_domain.py b/frappe/core/doctype/domain/test_domain.py
index c2686a7566..d7924ebc90 100644
--- a/frappe/core/doctype/domain/test_domain.py
+++ b/frappe/core/doctype/domain/test_domain.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/domain_settings/domain_settings.py b/frappe/core/doctype/domain_settings/domain_settings.py
index 7ad0aeff21..276411c2ab 100644
--- a/frappe/core/doctype/domain_settings/domain_settings.py
+++ b/frappe/core/doctype/domain_settings/domain_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
@@ -34,7 +34,7 @@ class DomainSettings(Document):
all_domains = list((frappe.get_hooks('domains') or {}))
def remove_role(role):
- frappe.db.sql('delete from `tabHas Role` where role=%s', role)
+ frappe.db.delete("Has Role", {"role": role})
frappe.set_value('Role', role, 'disabled', 1)
for domain in all_domains:
diff --git a/frappe/core/doctype/dynamic_link/dynamic_link.py b/frappe/core/doctype/dynamic_link/dynamic_link.py
index a7adb9ae72..c0502824c6 100644
--- a/frappe/core/doctype/dynamic_link/dynamic_link.py
+++ b/frappe/core/doctype/dynamic_link/dynamic_link.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/error_log/error_log.json b/frappe/core/doctype/error_log/error_log.json
index cdc7a63001..35ca3ceeef 100644
--- a/frappe/core/doctype/error_log/error_log.json
+++ b/frappe/core/doctype/error_log/error_log.json
@@ -112,7 +112,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2017-03-14 12:21:44.292471",
+ "modified": "2021-10-25 12:21:44.292471",
"modified_by": "Administrator",
"module": "Core",
"name": "Error Log",
@@ -144,6 +144,5 @@
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_order": "ASC",
- "track_changes": 1,
"track_seen": 0
-}
\ No newline at end of file
+}
diff --git a/frappe/core/doctype/error_log/error_log.py b/frappe/core/doctype/error_log/error_log.py
index 8223238c57..39c307520f 100644
--- a/frappe/core/doctype/error_log/error_log.py
+++ b/frappe/core/doctype/error_log/error_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
@@ -20,4 +20,4 @@ def set_old_logs_as_seen():
def clear_error_logs():
'''Flush all Error Logs'''
frappe.only_for('System Manager')
- frappe.db.sql('''DELETE FROM `tabError Log`''')
\ No newline at end of file
+ frappe.db.truncate("Error Log")
diff --git a/frappe/core/doctype/error_log/test_error_log.py b/frappe/core/doctype/error_log/test_error_log.py
index d7444ab2a7..54a41cd4a9 100644
--- a/frappe/core/doctype/error_log/test_error_log.py
+++ b/frappe/core/doctype/error_log/test_error_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/error_snapshot/error_snapshot.json b/frappe/core/doctype/error_snapshot/error_snapshot.json
index ea7a86d4f6..1333fe0d5b 100644
--- a/frappe/core/doctype/error_snapshot/error_snapshot.json
+++ b/frappe/core/doctype/error_snapshot/error_snapshot.json
@@ -359,7 +359,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2016-12-29 14:40:38.619106",
+ "modified": "2021-10-25 14:40:38.619106",
"modified_by": "Administrator",
"module": "Core",
"name": "Error Snapshot",
@@ -394,6 +394,5 @@
"sort_field": "timestamp",
"sort_order": "DESC",
"title_field": "evalue",
- "track_changes": 1,
"track_seen": 0
-}
\ No newline at end of file
+}
diff --git a/frappe/core/doctype/error_snapshot/error_snapshot.py b/frappe/core/doctype/error_snapshot/error_snapshot.py
index 247a796a6b..85143b5aa6 100644
--- a/frappe/core/doctype/error_snapshot/error_snapshot.py
+++ b/frappe/core/doctype/error_snapshot/error_snapshot.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/error_snapshot/test_error_snapshot.py b/frappe/core/doctype/error_snapshot/test_error_snapshot.py
index 135136294a..86928db9cc 100644
--- a/frappe/core/doctype/error_snapshot/test_error_snapshot.py
+++ b/frappe/core/doctype/error_snapshot/test_error_snapshot.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/chat/doctype/__init__.py b/frappe/core/doctype/feedback/__init__.py
similarity index 100%
rename from frappe/chat/doctype/__init__.py
rename to frappe/core/doctype/feedback/__init__.py
diff --git a/frappe/core/doctype/feedback/feedback.js b/frappe/core/doctype/feedback/feedback.js
new file mode 100644
index 0000000000..131f0e19d8
--- /dev/null
+++ b/frappe/core/doctype/feedback/feedback.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Feedback', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/core/doctype/feedback/feedback.json b/frappe/core/doctype/feedback/feedback.json
new file mode 100644
index 0000000000..b77e7a6677
--- /dev/null
+++ b/frappe/core/doctype/feedback/feedback.json
@@ -0,0 +1,87 @@
+{
+ "actions": [],
+ "creation": "2021-06-03 19:02:55.328423",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "reference_doctype",
+ "reference_name",
+ "column_break_3",
+ "rating",
+ "ip_address",
+ "section_break_6",
+ "feedback"
+ ],
+ "fields": [
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "rating",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Rating",
+ "precision": "1",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "feedback",
+ "fieldtype": "Small Text",
+ "label": "Feedback",
+ "reqd": 1
+ },
+ {
+ "fieldname": "reference_doctype",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Reference Document Type",
+ "options": "\nBlog Post"
+ },
+ {
+ "fieldname": "reference_name",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "label": "Reference Name",
+ "options": "reference_doctype",
+ "reqd": 1
+ },
+ {
+ "fieldname": "ip_address",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "IP Address",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-06-23 12:45:42.045696",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Feedback",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "reference_name",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/feedback/feedback.py b/frappe/core/doctype/feedback/feedback.py
new file mode 100644
index 0000000000..3704ee66e0
--- /dev/null
+++ b/frappe/core/doctype/feedback/feedback.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# License: MIT. See LICENSE
+
+# import frappe
+from frappe.model.document import Document
+
+class Feedback(Document):
+ pass
diff --git a/frappe/core/doctype/feedback/test_feedback.py b/frappe/core/doctype/feedback/test_feedback.py
new file mode 100644
index 0000000000..f3cf8dfe6b
--- /dev/null
+++ b/frappe/core/doctype/feedback/test_feedback.py
@@ -0,0 +1,41 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# License: MIT. See LICENSE
+
+import frappe
+import unittest
+
+class TestFeedback(unittest.TestCase):
+ def tearDown(self):
+ frappe.form_dict.reference_doctype = None
+ frappe.form_dict.reference_name = None
+ frappe.form_dict.rating = None
+ frappe.form_dict.feedback = None
+ frappe.local.request_ip = None
+
+ def test_feedback_creation_updation(self):
+ from frappe.website.doctype.blog_post.test_blog_post import make_test_blog
+ test_blog = make_test_blog()
+
+ frappe.db.delete("Feedback", {"reference_doctype": "Blog Post"})
+
+ from frappe.templates.includes.feedback.feedback import add_feedback, update_feedback
+
+ frappe.form_dict.reference_doctype = 'Blog Post'
+ frappe.form_dict.reference_name = test_blog.name
+ frappe.form_dict.rating = 5
+ frappe.form_dict.feedback = 'New feedback'
+ frappe.local.request_ip = '127.0.0.1'
+
+ feedback = add_feedback()
+
+ self.assertEqual(feedback.feedback, 'New feedback')
+ self.assertEqual(feedback.rating, 5)
+
+ updated_feedback = update_feedback('Blog Post', test_blog.name, 6, 'Updated feedback')
+
+ self.assertEqual(updated_feedback.feedback, 'Updated feedback')
+ self.assertEqual(updated_feedback.rating, 6)
+
+ frappe.db.delete("Feedback", {"reference_doctype": "Blog Post"})
+
+ test_blog.delete()
\ No newline at end of file
diff --git a/frappe/core/doctype/file/file.js b/frappe/core/doctype/file/file.js
index 6d77cb91ad..d40328d3cd 100644
--- a/frappe/core/doctype/file/file.js
+++ b/frappe/core/doctype/file/file.js
@@ -23,6 +23,18 @@ frappe.ui.form.on("File", "refresh", function(frm) {
wrapper.empty();
}
+ var is_raster_image = (/\.(gif|jpg|jpeg|tiff|png)$/i).test(frm.doc.file_url);
+ var is_optimizable = !frm.doc.is_folder && is_raster_image && frm.doc.file_size > 0;
+
+ if (is_optimizable) {
+ frm.add_custom_button(__("Optimize"), function() {
+ frappe.show_alert(__("Optimizing image..."));
+ frm.call("optimize_file").then(() => {
+ frappe.show_alert(__("Image optimized"));
+ });
+ });
+ }
+
if(frm.doc.file_name && frm.doc.file_name.split('.').splice(-1)[0]==='zip') {
frm.add_custom_button(__('Unzip'), function() {
frappe.call({
diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py
index b4bfe1d21b..4df9ef3132 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
"""
record of files
@@ -21,14 +21,14 @@ import zipfile
import requests
import requests.exceptions
from PIL import Image, ImageFile, ImageOps
-from io import StringIO
+from io import BytesIO
from urllib.parse import quote, unquote
import frappe
-from frappe import _, conf
+from frappe import _, conf, safe_decode
from frappe.model.document import Document
from frappe.utils import call_hook_method, cint, cstr, encode, get_files_path, get_hook_method, random_string, strip
-from frappe.utils.image import strip_exif_data
+from frappe.utils.image import strip_exif_data, optimize_image
class MaxFileSizeReachedError(frappe.ValidationError):
pass
@@ -254,11 +254,11 @@ class File(Document):
return
file_name = self.file_url.split('/')[-1]
try:
- with open(get_files_path(file_name, is_private=self.is_private), "rb") as f:
+ file_path = get_files_path(file_name, is_private=self.is_private)
+ with open(file_path, "rb") as f:
self.content_hash = get_content_hash(f.read())
except IOError:
- frappe.msgprint(_("File {0} does not exist").format(self.file_url))
- raise
+ frappe.throw(_("File {0} does not exist").format(file_path))
def on_trash(self):
if self.is_home_folder or self.is_attachments_folder:
@@ -270,16 +270,12 @@ class File(Document):
def make_thumbnail(self, set_as_thumbnail=True, width=300, height=300, suffix="small", crop=False):
if self.file_url:
- if self.file_url.startswith("/files"):
- try:
+ try:
+ if self.file_url.startswith(("/files", "/private/files")):
image, filename, extn = get_local_image(self.file_url)
- except IOError:
- return
-
- else:
- try:
+ else:
image, filename, extn = get_web_image(self.file_url)
- except (requests.exceptions.HTTPError, requests.exceptions.SSLError, IOError, TypeError):
+ except (requests.exceptions.HTTPError, requests.exceptions.SSLError, IOError, TypeError):
return
size = width, height
@@ -289,16 +285,13 @@ class File(Document):
image.thumbnail(size, Image.ANTIALIAS)
thumbnail_url = filename + "_" + suffix + "." + extn
-
path = os.path.abspath(frappe.get_site_path("public", thumbnail_url.lstrip("/")))
try:
image.save(path)
-
if set_as_thumbnail:
self.db_set("thumbnail_url", thumbnail_url)
- self.db_set("thumbnail_url", thumbnail_url)
except IOError:
frappe.msgprint(_("Unable to write file format for {0}").format(path))
return
@@ -321,17 +314,23 @@ class File(Document):
self.delete_file_data_content(only_thumbnail=True)
def on_rollback(self):
- self.flags.on_rollback = True
- self.on_trash()
+ # if original_content flag is set, this rollback should revert the file to its original state
+ if self.flags.original_content:
+ file_path = self.get_full_path()
+ with open(file_path, "wb+") as f:
+ f.write(self.flags.original_content)
+
+ # following condition is only executed when an insert has been rolledback
+ else:
+ self.flags.on_rollback = True
+ self.on_trash()
def unzip(self):
'''Unzip current file and replace it by its children'''
- if not ".zip" in self.file_name:
- frappe.msgprint(_("Not a zip file"))
- return
+ if not self.file_url.endswith(".zip"):
+ frappe.throw(_("{0} is not a zip file").format(self.file_name))
- zip_path = frappe.get_site_path(self.file_url.strip('/'))
- base_url = os.path.dirname(self.file_url)
+ zip_path = self.get_full_path()
files = []
with zipfile.ZipFile(zip_path) as z:
@@ -359,10 +358,6 @@ class File(Document):
return files
- def get_file_url(self):
- data = frappe.db.get_value("File", self.file_data_name, ["file_name", "file_url"], as_dict=True)
- return data.file_url or data.file_name
-
def exists_on_disk(self):
exists = os.path.exists(self.get_full_path())
return exists
@@ -431,47 +426,6 @@ class File(Document):
return get_files_path(self.file_name, is_private=self.is_private)
- def get_file_doc(self):
- '''returns File object (Document) from given parameters or form_dict'''
- r = frappe.form_dict
-
- if self.file_url is None: self.file_url = r.file_url
- if self.file_name is None: self.file_name = r.file_name
- if self.attached_to_doctype is None: self.attached_to_doctype = r.doctype
- if self.attached_to_name is None: self.attached_to_name = r.docname
- if self.attached_to_field is None: self.attached_to_field = r.docfield
- if self.folder is None: self.folder = r.folder
- if self.is_private is None: self.is_private = r.is_private
-
- if r.filedata:
- file_doc = self.save_uploaded()
-
- elif r.file_url:
- file_doc = self.save()
-
- return file_doc
-
-
- def save_uploaded(self):
- self.content = self.get_uploaded_content()
- if self.content:
- return self.save()
- else:
- raise Exception
-
- def get_uploaded_content(self):
- # should not be unicode when reading a file, hence using frappe.form
- if 'filedata' in frappe.form_dict:
- if "," in frappe.form_dict.filedata:
- frappe.form_dict.filedata = frappe.form_dict.filedata.rsplit(",", 1)[1]
- frappe.uploaded_content = base64.b64decode(frappe.form_dict.filedata)
- return frappe.uploaded_content
- elif self.content:
- return self.content
- frappe.msgprint(_('No file attached'))
- return None
-
-
def save_file(self, content=None, decode=False, ignore_existing_file_check=False):
file_exists = False
self.content = content
@@ -539,14 +493,6 @@ class File(Document):
'file_url': self.file_url
}
- def get_file_data_from_hash(self):
- for name in frappe.db.sql_list("select name from `tabFile` where content_hash=%s and is_private=%s",
- (self.content_hash, self.is_private)):
- b = frappe.get_doc('File', name)
- return {k: b.get(k) for k in frappe.get_hooks()['write_file_keys']}
- return False
-
-
def check_max_file_size(self):
max_file_size = get_max_file_size()
file_size = len(self.content)
@@ -594,6 +540,35 @@ class File(Document):
if self.file_url:
self.is_private = cint(self.file_url.startswith('/private'))
+ @frappe.whitelist()
+ def optimize_file(self):
+ if self.is_folder:
+ raise TypeError('Folders cannot be optimized')
+
+ content_type = mimetypes.guess_type(self.file_name)[0]
+ is_local_image = content_type.startswith('image/') and self.file_size > 0
+ is_svg = content_type == 'image/svg+xml'
+
+ if not is_local_image:
+ raise NotImplementedError('Only local image files can be optimized')
+
+ if is_svg:
+ raise TypeError('Optimization of SVG images is not supported')
+
+ content = self.get_content()
+ file_path = self.get_full_path()
+ optimized_content = optimize_image(content, content_type)
+
+ with open(file_path, 'wb+') as f:
+ f.write(optimized_content)
+
+ self.file_size = len(optimized_content)
+ self.content_hash = get_content_hash(optimized_content)
+ # if rolledback, revert back to original
+ self.flags.original_content = content
+ frappe.local.rollback_observers.append(self)
+ self.save()
+
def on_doctype_update():
frappe.db.add_index("File", ["attached_to_doctype", "attached_to_name"])
@@ -621,7 +596,8 @@ def create_new_folder(file_name, folder):
file.file_name = file_name
file.is_folder = 1
file.folder = folder
- file.insert()
+ file.insert(ignore_if_duplicate=True)
+ return file
@frappe.whitelist()
def move_file(file_list, new_parent, old_parent):
@@ -672,7 +648,7 @@ def get_local_image(file_url):
try:
image = Image.open(file_path)
except IOError:
- frappe.msgprint(_("Unable to read file format for {0}").format(file_url), raise_exception=True)
+ frappe.throw(_("Unable to read file format for {0}").format(file_url))
content = None
@@ -703,7 +679,10 @@ def get_web_image(file_url):
frappe.msgprint(_("Unable to read file format for {0}").format(file_url))
raise
- image = Image.open(StringIO(frappe.safe_decode(r.content)))
+ try:
+ image = Image.open(BytesIO(r.content))
+ except Exception as e:
+ frappe.msgprint(_("Image link '{0}' is not valid").format(file_url), raise_exception=e)
try:
filename, extn = file_url.rsplit("/", 1)[1].rsplit(".", 1)
@@ -737,48 +716,12 @@ def delete_file(path):
os.remove(path)
-def remove_file(fid=None, attached_to_doctype=None, attached_to_name=None, from_delete=False, delete_permanently=False):
- """Remove file and File entry"""
- file_name = None
- if not (attached_to_doctype and attached_to_name):
- attached = frappe.db.get_value("File", fid,
- ["attached_to_doctype", "attached_to_name", "file_name"])
- if attached:
- attached_to_doctype, attached_to_name, file_name = attached
-
- ignore_permissions, comment = False, None
- if attached_to_doctype and attached_to_name and not from_delete:
- doc = frappe.get_doc(attached_to_doctype, attached_to_name)
- ignore_permissions = doc.has_permission("write") or False
- if frappe.flags.in_web_form:
- ignore_permissions = True
- if not file_name:
- file_name = frappe.db.get_value("File", fid, "file_name")
- comment = doc.add_comment("Attachment Removed", _("Removed {0}").format(file_name))
- frappe.delete_doc("File", fid, ignore_permissions=ignore_permissions, delete_permanently=delete_permanently)
-
- return comment
def get_max_file_size():
return cint(conf.get('max_file_size')) or 10485760
-def remove_all(dt, dn, from_delete=False, delete_permanently=False):
- """remove all files in a transaction"""
- try:
- for fid in frappe.db.sql_list("""select name from `tabFile` where
- attached_to_doctype=%s and attached_to_name=%s""", (dt, dn)):
- if from_delete:
- # If deleting a doc, directly delete files
- frappe.delete_doc("File", fid, ignore_permissions=True, delete_permanently=delete_permanently)
- else:
- # Removes file and adds a comment in the document it is attached to
- remove_file(fid=fid, attached_to_doctype=dt, attached_to_name=dn,
- from_delete=from_delete, delete_permanently=delete_permanently)
- except Exception as e:
- if e.args[0]!=1054: raise # (temp till for patched)
-
def has_permission(doc, ptype=None, user=None):
has_access = False
@@ -824,6 +767,7 @@ def remove_file_by_url(file_url, doctype=None, name=None):
fid = frappe.db.get_value("File", {"file_url": file_url})
if fid:
+ from frappe.utils.file_manager import remove_file
return remove_file(fid=fid)
@@ -869,22 +813,28 @@ def extract_images_from_doc(doc, fieldname):
doc.set(fieldname, content)
-def extract_images_from_html(doc, content):
+def extract_images_from_html(doc, content, is_private=False):
frappe.flags.has_dataurl = False
def _save_file(match):
data = match.group(1)
data = data.split("data:")[1]
headers, content = data.split(",")
+ mtype = headers.split(";")[0]
+
+ if isinstance(content, str):
+ content = content.encode("utf-8")
+ if b"," in content:
+ content = content.split(b",")[1]
+ content = base64.b64decode(content)
+
+ content = optimize_image(content, mtype)
if "filename=" in headers:
filename = headers.split("filename=")[-1]
+ filename = safe_decode(filename).split(";")[0]
- # decode filename
- if not isinstance(filename, str):
- filename = str(filename, 'utf-8')
else:
- mtype = headers.split(";")[0]
filename = get_random_filename(content_type=mtype)
doctype = doc.parenttype if doc.parent else doc.doctype
@@ -896,7 +846,8 @@ def extract_images_from_html(doc, content):
"attached_to_doctype": doctype,
"attached_to_name": name,
"content": content,
- "decode": True
+ "decode": False,
+ "is_private": is_private
})
_file.save(ignore_permissions=True)
file_url = _file.file_url
@@ -911,12 +862,9 @@ def extract_images_from_html(doc, content):
return content
-def get_random_filename(extn=None, content_type=None):
- if extn:
- if not extn.startswith("."):
- extn = "." + extn
-
- elif content_type:
+def get_random_filename(content_type=None):
+ extn = None
+ if content_type:
extn = mimetypes.guess_extension(content_type)
return random_string(7) + (extn or "")
@@ -927,7 +875,7 @@ def unzip_file(name):
'''Unzip the given file and make file records for each of the extracted files'''
file_obj = frappe.get_doc('File', name)
files = file_obj.unzip()
- return len(files)
+ return files
@frappe.whitelist()
@@ -952,13 +900,6 @@ def get_attached_images(doctype, names):
return out
-@frappe.whitelist()
-def validate_filename(filename):
- from frappe.utils import now_datetime
- timestamp = now_datetime().strftime(" %Y-%m-%d %H:%M:%S")
- fname = get_file_name(filename, timestamp)
- return fname
-
@frappe.whitelist()
def get_files_in_folder(folder, start=0, page_length=20):
start = cint(start)
diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py
index 649010c468..9a758b53f5 100644
--- a/frappe/core/doctype/file/test_file.py
+++ b/frappe/core/doctype/file/test_file.py
@@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import base64
+import json
import frappe
import os
import unittest
from frappe import _
-from frappe.core.doctype.file.file import move_file, get_files_in_folder
+from frappe.core.doctype.file.file import get_attached_images, move_file, get_files_in_folder, unzip_file
from frappe.utils import get_files_path
# test_records = frappe.get_test_records('File')
@@ -203,10 +204,14 @@ class TestFile(unittest.TestCase):
def delete_test_data(self):
- for f in frappe.db.sql('''select name, file_name from tabFile where
- is_home_folder = 0 and is_attachments_folder = 0 order by creation desc'''):
- frappe.delete_doc("File", f[0])
-
+ test_file_data = frappe.db.get_all(
+ "File",
+ pluck="name",
+ filters={"is_home_folder": 0, "is_attachments_folder": 0},
+ order_by="creation desc",
+ )
+ for f in test_file_data:
+ frappe.delete_doc("File", f)
def upload_file(self):
_file = frappe.get_doc({
@@ -365,6 +370,81 @@ class TestFile(unittest.TestCase):
file1.file_url = '/private/files/parent_dir2.txt'
file1.save()
+ def test_file_url_validation(self):
+ test_file = frappe.get_doc({
+ "doctype": "File",
+ "file_name": 'logo',
+ "file_url": 'https://frappe.io/files/frappe.png'
+ })
+
+ self.assertIsNone(test_file.validate())
+
+ # bad path
+ test_file.file_url = "/usr/bin/man"
+ self.assertRaisesRegex(frappe.exceptions.ValidationError, "URL must start with http:// or https://", test_file.validate)
+
+ test_file.file_url = None
+ test_file.file_name = "/usr/bin/man"
+ self.assertRaisesRegex(frappe.exceptions.ValidationError, "There is some problem with the file url", test_file.validate)
+
+ test_file.file_url = None
+ test_file.file_name = "_file"
+ self.assertRaisesRegex(IOError, "does not exist", test_file.validate)
+
+ test_file.file_url = None
+ test_file.file_name = "/private/files/_file"
+ self.assertRaisesRegex(IOError, "does not exist", test_file.validate)
+
+ def test_make_thumbnail(self):
+ # test web image
+ test_file = frappe.get_doc({
+ "doctype": "File",
+ "file_name": 'logo',
+ "file_url": frappe.utils.get_url('/_test/assets/image.jpg'),
+ }).insert(ignore_permissions=True)
+
+ test_file.make_thumbnail()
+ self.assertEquals(test_file.thumbnail_url, '/files/image_small.jpg')
+
+ # test local image
+ test_file.db_set('thumbnail_url', None)
+ test_file.reload()
+ test_file.file_url = "/files/image_small.jpg"
+ test_file.make_thumbnail(suffix="xs", crop=True)
+ self.assertEquals(test_file.thumbnail_url, '/files/image_small_xs.jpg')
+
+ frappe.clear_messages()
+ test_file.db_set('thumbnail_url', None)
+ test_file.reload()
+ test_file.file_url = frappe.utils.get_url('unknown.jpg')
+ test_file.make_thumbnail(suffix="xs")
+ self.assertEqual(json.loads(frappe.message_log[0]), {"message": f"File '{frappe.utils.get_url('unknown.jpg')}' not found"})
+ self.assertEquals(test_file.thumbnail_url, None)
+
+ def test_file_unzip(self):
+ file_path = frappe.get_app_path('frappe', 'www/_test/assets/file.zip')
+ public_file_path = frappe.get_site_path('public', 'files')
+ try:
+ import shutil
+ shutil.copy(file_path, public_file_path)
+ except Exception:
+ pass
+
+ test_file = frappe.get_doc({
+ "doctype": "File",
+ "file_url": '/files/file.zip',
+ }).insert(ignore_permissions=True)
+
+ self.assertListEqual([file.file_name for file in unzip_file(test_file.name)],
+ ['css_asset.css', 'image.jpg', 'js_asset.min.js'])
+
+ test_file = frappe.get_doc({
+ "doctype": "File",
+ "file_url": frappe.utils.get_url('/_test/assets/image.jpg'),
+ }).insert(ignore_permissions=True)
+ self.assertRaisesRegex(frappe.exceptions.ValidationError, 'not a zip file', test_file.unzip)
+
+
class TestAttachment(unittest.TestCase):
test_doctype = 'Test For Attachment'
@@ -469,3 +549,93 @@ class TestAttachmentsAccess(unittest.TestCase):
frappe.set_user('Administrator')
frappe.db.rollback()
+
+
+class TestFileUtils(unittest.TestCase):
+ def test_extract_images_from_doc(self):
+ # with filename in data URI
+ todo = frappe.get_doc({
+ "doctype": "ToDo",
+ "description": 'Test

'
+ }).insert()
+ self.assertTrue(frappe.db.exists("File", {"attached_to_name": todo.name}))
+ self.assertIn('

', todo.description)
+ self.assertListEqual(get_attached_images('ToDo', [todo.name])[todo.name], ['/files/pix.png'])
+
+ # without filename in data URI
+ todo = frappe.get_doc({
+ "doctype": "ToDo",
+ "description": 'Test

'
+ }).insert()
+ filename = frappe.db.exists("File", {"attached_to_name": todo.name})
+ self.assertIn(f'

Error Logs ')
}
- if frappe.db.sql_list("select name from `tabError Log` where seen = 0 limit 1"):
+ if frappe.get_all("Error Log", filters={"seen": 0}, limit=1):
log_settings = frappe.get_cached_doc('Log Settings')
if log_settings.users_to_notify:
diff --git a/frappe/core/doctype/log_settings/test_log_settings.py b/frappe/core/doctype/log_settings/test_log_settings.py
index 8e0c9c3f23..40287948fd 100644
--- a/frappe/core/doctype/log_settings/test_log_settings.py
+++ b/frappe/core/doctype/log_settings/test_log_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/core/doctype/module_def/__init__.py b/frappe/core/doctype/module_def/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/core/doctype/module_def/__init__.py
+++ b/frappe/core/doctype/module_def/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/core/doctype/module_def/module_def.js b/frappe/core/doctype/module_def/module_def.js
index c7a6cf85f9..73d2d6562c 100644
--- a/frappe/core/doctype/module_def/module_def.js
+++ b/frappe/core/doctype/module_def/module_def.js
@@ -5,6 +5,9 @@ frappe.ui.form.on('Module Def', {
refresh: function(frm) {
frappe.xcall('frappe.core.doctype.module_def.module_def.get_installed_apps').then(r => {
frm.set_df_property('app_name', 'options', JSON.parse(r));
+ if (!frm.doc.app_name) {
+ frm.set_value('app_name', 'frappe');
+ }
});
}
});
diff --git a/frappe/core/doctype/module_def/module_def.json b/frappe/core/doctype/module_def/module_def.json
index 4de046bbb6..7ddc55fce5 100644
--- a/frappe/core/doctype/module_def/module_def.json
+++ b/frappe/core/doctype/module_def/module_def.json
@@ -8,6 +8,7 @@
"field_order": [
"module_name",
"custom",
+ "package",
"app_name",
"restrict_to_domain"
],
@@ -23,6 +24,7 @@
"unique": 1
},
{
+ "depends_on": "eval:!doc.custom",
"fieldname": "app_name",
"fieldtype": "Select",
"in_list_view": 1,
@@ -41,24 +43,84 @@
"fieldname": "custom",
"fieldtype": "Check",
"label": "Custom"
+ },
+ {
+ "depends_on": "custom",
+ "fieldname": "package",
+ "fieldtype": "Link",
+ "label": "Package",
+ "options": "Package"
}
],
"icon": "fa fa-sitemap",
"idx": 1,
"links": [
{
+ "group": "DocType",
"link_doctype": "DocType",
"link_fieldname": "module"
},
{
+ "group": "DocType",
+ "link_doctype": "Client Script",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "DocType",
+ "link_doctype": "Server Script",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Website",
+ "link_doctype": "Web Page",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Website",
+ "link_doctype": "Web Template",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Website",
+ "link_doctype": "Website Theme",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Website",
+ "link_doctype": "Web Form",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Customization",
"link_doctype": "Workspace",
"link_fieldname": "module"
+ },
+ {
+ "group": "Customization",
+ "link_doctype": "Custom Field",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Customization",
+ "link_doctype": "Property Setter",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Customization",
+ "link_doctype": "Print Format",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Customization",
+ "link_doctype": "Notification",
+ "link_fieldname": "module"
}
],
- "modified": "2021-06-02 13:04:53.118716",
+ "modified": "2021-09-05 21:58:40.253909",
"modified_by": "Administrator",
"module": "Core",
"name": "Module Def",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/core/doctype/module_def/module_def.py b/frappe/core/doctype/module_def/module_def.py
index 68025c83bb..6b420430b8 100644
--- a/frappe/core/doctype/module_def/module_def.py
+++ b/frappe/core/doctype/module_def/module_def.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe, os, json
diff --git a/frappe/core/doctype/module_def/test_module_def.py b/frappe/core/doctype/module_def/test_module_def.py
index 3a3ceb4b57..69a114d765 100644
--- a/frappe/core/doctype/module_def/test_module_def.py
+++ b/frappe/core/doctype/module_def/test_module_def.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/module_profile/module_profile.py b/frappe/core/doctype/module_profile/module_profile.py
index 373e5078d0..930c3879b6 100644
--- a/frappe/core/doctype/module_profile/module_profile.py
+++ b/frappe/core/doctype/module_profile/module_profile.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from frappe.model.document import Document
diff --git a/frappe/core/doctype/module_profile/test_module_profile.py b/frappe/core/doctype/module_profile/test_module_profile.py
index e0d9c13371..e676767db6 100644
--- a/frappe/core/doctype/module_profile/test_module_profile.py
+++ b/frappe/core/doctype/module_profile/test_module_profile.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/navbar_item/navbar_item.py b/frappe/core/doctype/navbar_item/navbar_item.py
index a8fa611374..d4952a75f2 100644
--- a/frappe/core/doctype/navbar_item/navbar_item.py
+++ b/frappe/core/doctype/navbar_item/navbar_item.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/navbar_item/test_navbar_item.py b/frappe/core/doctype/navbar_item/test_navbar_item.py
index 85852a45e8..bb4b2a837a 100644
--- a/frappe/core/doctype/navbar_item/test_navbar_item.py
+++ b/frappe/core/doctype/navbar_item/test_navbar_item.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/core/doctype/navbar_settings/navbar_settings.py b/frappe/core/doctype/navbar_settings/navbar_settings.py
index 60aec67a00..46eb5c3e7a 100644
--- a/frappe/core/doctype/navbar_settings/navbar_settings.py
+++ b/frappe/core/doctype/navbar_settings/navbar_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
@@ -22,7 +22,6 @@ class NavbarSettings(Document):
if not frappe.flags.in_patch and (len(before_save_items) > len(after_save_items)):
frappe.throw(_("Please hide the standard navbar items instead of deleting them"))
-@frappe.whitelist(allow_guest=True)
def get_app_logo():
app_logo = frappe.db.get_single_value('Navbar Settings', 'app_logo', cache=True)
if not app_logo:
diff --git a/frappe/core/doctype/navbar_settings/test_navbar_settings.py b/frappe/core/doctype/navbar_settings/test_navbar_settings.py
index 4d1ee72815..01497d9035 100644
--- a/frappe/core/doctype/navbar_settings/test_navbar_settings.py
+++ b/frappe/core/doctype/navbar_settings/test_navbar_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/chat/doctype/chat_message/__init__.py b/frappe/core/doctype/package/__init__.py
similarity index 100%
rename from frappe/chat/doctype/chat_message/__init__.py
rename to frappe/core/doctype/package/__init__.py
diff --git a/frappe/core/doctype/package/licenses/GNU Affero General Public License.md b/frappe/core/doctype/package/licenses/GNU Affero General Public License.md
new file mode 100644
index 0000000000..c7f159aed8
--- /dev/null
+++ b/frappe/core/doctype/package/licenses/GNU Affero General Public License.md
@@ -0,0 +1,614 @@
+### GNU AFFERO GENERAL PUBLIC LICENSE
+
+Version 3, 19 November 2007
+
+Copyright (C) 2007 Free Software Foundation, Inc.
+
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+### Preamble
+
+The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains
+free software for all its users.
+
+When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing
+under this license.
+
+The precise terms and conditions for copying, distribution and
+modification follow.
+
+### TERMS AND CONDITIONS
+
+#### 0. Definitions.
+
+"This License" refers to version 3 of the GNU Affero General Public
+License.
+
+"Copyright" also means copyright-like laws that apply to other kinds
+of works, such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of
+an exact copy. The resulting work is called a "modified version" of
+the earlier work or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user
+through a computer network, with no transfer of a copy, is not
+conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to
+the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+#### 1. Source Code.
+
+The "source code" for a work means the preferred form of the work for
+making modifications to it. "Object code" means any non-source form of
+a work.
+
+A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can
+regenerate automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same
+work.
+
+#### 2. Basic Permissions.
+
+All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not convey,
+without conditions so long as your license otherwise remains in force.
+You may convey covered works to others for the sole purpose of having
+them make modifications exclusively for you, or provide you with
+facilities for running those works, provided that you comply with the
+terms of this License in conveying all material for which you do not
+control copyright. Those thus making or running the covered works for
+you must do so exclusively on your behalf, under your direction and
+control, on terms that prohibit them from making any copies of your
+copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the
+conditions stated below. Sublicensing is not allowed; section 10 makes
+it unnecessary.
+
+#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such
+circumvention is effected by exercising rights under this License with
+respect to the covered work, and you disclaim any intention to limit
+operation or modification of the work as a means of enforcing, against
+the work's users, your or third parties' legal rights to forbid
+circumvention of technological measures.
+
+#### 4. Conveying Verbatim Copies.
+
+You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+#### 5. Conveying Modified Source Versions.
+
+You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these
+conditions:
+
+- a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+- b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under
+ section 7. This requirement modifies the requirement in section 4
+ to "keep intact all notices".
+- c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+- d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+#### 6. Conveying Non-Source Forms.
+
+You may convey a covered work in object code form under the terms of
+sections 4 and 5, provided that you also convey the machine-readable
+Corresponding Source under the terms of this License, in one of these
+ways:
+
+- a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+- b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the Corresponding
+ Source from a network server at no charge.
+- c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+- d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+- e) Convey the object code using peer-to-peer transmission,
+ provided you inform other peers where the object code and
+ Corresponding Source of the work are being offered to the general
+ public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal,
+family, or household purposes, or (2) anything designed or sold for
+incorporation into a dwelling. In determining whether a product is a
+consumer product, doubtful cases shall be resolved in favor of
+coverage. For a particular product received by a particular user,
+"normally used" refers to a typical or common use of that class of
+product, regardless of the status of the particular user or of the way
+in which the particular user actually uses, or expects or is expected
+to use, the product. A product is a consumer product regardless of
+whether the product has substantial commercial, industrial or
+non-consumer uses, unless such uses represent the only significant
+mode of use of the product.
+
+"Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to
+install and execute modified versions of a covered work in that User
+Product from a modified version of its Corresponding Source. The
+information must suffice to ensure that the continued functioning of
+the modified object code is in no case prevented or interfered with
+solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or
+updates for a work that has been modified or installed by the
+recipient, or for the User Product in which it has been modified or
+installed. Access to a network may be denied when the modification
+itself materially and adversely affects the operation of the network
+or violates the rules and protocols for communication across the
+network.
+
+Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+#### 7. Additional Terms.
+
+"Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders
+of that material) supplement the terms of this License with terms:
+
+- a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+- b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+- c) Prohibiting misrepresentation of the origin of that material,
+ or requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+- d) Limiting the use for publicity purposes of names of licensors
+ or authors of the material; or
+- e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+- f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions
+ of it) with contractual assumptions of liability to the recipient,
+ for any liability that these contractual assumptions directly
+ impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions; the
+above requirements apply either way.
+
+#### 8. Termination.
+
+You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+However, if you cease all violation of this License, then your license
+from a particular copyright holder is reinstated (a) provisionally,
+unless and until the copyright holder explicitly and finally
+terminates your license, and (b) permanently, if the copyright holder
+fails to notify you of the violation by some reasonable means prior to
+60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+#### 9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run
+a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+#### 10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+#### 11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims owned
+or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the
+scope of its coverage, prohibits the exercise of, or is conditioned on
+the non-exercise of one or more of the rights that are specifically
+granted under this License. You may not convey a covered work if you
+are a party to an arrangement with a third party that is in the
+business of distributing software, under which you make payment to the
+third party based on the extent of your activity of conveying the
+work, and under which the third party grants, to any of the parties
+who would receive the covered work from you, a discriminatory patent
+license (a) in connection with copies of the covered work conveyed by
+you (or copies made from those copies), or (b) primarily for and in
+connection with specific products or compilations that contain the
+covered work, unless you entered into that arrangement, or that patent
+license was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+#### 12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under
+this License and any other pertinent obligations, then as a
+consequence you may not convey it at all. For example, if you agree to
+terms that obligate you to collect a royalty for further conveying
+from those to whom you convey the Program, the only way you could
+satisfy both those terms and this License would be to refrain entirely
+from conveying the Program.
+
+#### 13. Remote Network Interaction; Use with the GNU General Public License.
+
+Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your
+version supports such interaction) an opportunity to receive the
+Corresponding Source of your version by providing access to the
+Corresponding Source from a network server at no charge, through some
+standard or customary means of facilitating copying of software. This
+Corresponding Source shall include the Corresponding Source for any
+work covered by version 3 of the GNU General Public License that is
+incorporated pursuant to the following paragraph.
+
+Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+#### 14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions
+of the GNU Affero General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever
+published by the Free Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions
+of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+#### 15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
+WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
+PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
+DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
+CORRECTION.
+
+#### 16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
+CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
+ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
+NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
+LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
+TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
+PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+#### 17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
diff --git a/frappe/core/doctype/package/licenses/GNU General Public License.md b/frappe/core/doctype/package/licenses/GNU General Public License.md
new file mode 100644
index 0000000000..c4580f2eb6
--- /dev/null
+++ b/frappe/core/doctype/package/licenses/GNU General Public License.md
@@ -0,0 +1,617 @@
+### GNU GENERAL PUBLIC LICENSE
+
+Version 3, 29 June 2007
+
+Copyright (C) 2007 Free Software Foundation, Inc.
+
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+### Preamble
+
+The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom
+to share and change all versions of a program--to make sure it remains
+free software for all its users. We, the Free Software Foundation, use
+the GNU General Public License for most of our software; it applies
+also to any other work released this way by its authors. You can apply
+it to your programs, too.
+
+When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you
+have certain responsibilities if you distribute copies of the
+software, or if you modify it: responsibilities to respect the freedom
+of others.
+
+For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the
+manufacturer can do so. This is fundamentally incompatible with the
+aim of protecting users' freedom to change the software. The
+systematic pattern of such abuse occurs in the area of products for
+individuals to use, which is precisely where it is most unacceptable.
+Therefore, we have designed this version of the GPL to prohibit the
+practice for those products. If such problems arise substantially in
+other domains, we stand ready to extend this provision to those
+domains in future versions of the GPL, as needed to protect the
+freedom of users.
+
+Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish
+to avoid the special danger that patents applied to a free program
+could make it effectively proprietary. To prevent this, the GPL
+assures that patents cannot be used to render the program non-free.
+
+The precise terms and conditions for copying, distribution and
+modification follow.
+
+### TERMS AND CONDITIONS
+
+#### 0. Definitions.
+
+"This License" refers to version 3 of the GNU General Public License.
+
+"Copyright" also means copyright-like laws that apply to other kinds
+of works, such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of
+an exact copy. The resulting work is called a "modified version" of
+the earlier work or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user
+through a computer network, with no transfer of a copy, is not
+conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to
+the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+#### 1. Source Code.
+
+The "source code" for a work means the preferred form of the work for
+making modifications to it. "Object code" means any non-source form of
+a work.
+
+A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can
+regenerate automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same
+work.
+
+#### 2. Basic Permissions.
+
+All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not convey,
+without conditions so long as your license otherwise remains in force.
+You may convey covered works to others for the sole purpose of having
+them make modifications exclusively for you, or provide you with
+facilities for running those works, provided that you comply with the
+terms of this License in conveying all material for which you do not
+control copyright. Those thus making or running the covered works for
+you must do so exclusively on your behalf, under your direction and
+control, on terms that prohibit them from making any copies of your
+copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the
+conditions stated below. Sublicensing is not allowed; section 10 makes
+it unnecessary.
+
+#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such
+circumvention is effected by exercising rights under this License with
+respect to the covered work, and you disclaim any intention to limit
+operation or modification of the work as a means of enforcing, against
+the work's users, your or third parties' legal rights to forbid
+circumvention of technological measures.
+
+#### 4. Conveying Verbatim Copies.
+
+You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+#### 5. Conveying Modified Source Versions.
+
+You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these
+conditions:
+
+- a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+- b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under
+ section 7. This requirement modifies the requirement in section 4
+ to "keep intact all notices".
+- c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+- d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+#### 6. Conveying Non-Source Forms.
+
+You may convey a covered work in object code form under the terms of
+sections 4 and 5, provided that you also convey the machine-readable
+Corresponding Source under the terms of this License, in one of these
+ways:
+
+- a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+- b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the Corresponding
+ Source from a network server at no charge.
+- c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+- d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+- e) Convey the object code using peer-to-peer transmission,
+ provided you inform other peers where the object code and
+ Corresponding Source of the work are being offered to the general
+ public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal,
+family, or household purposes, or (2) anything designed or sold for
+incorporation into a dwelling. In determining whether a product is a
+consumer product, doubtful cases shall be resolved in favor of
+coverage. For a particular product received by a particular user,
+"normally used" refers to a typical or common use of that class of
+product, regardless of the status of the particular user or of the way
+in which the particular user actually uses, or expects or is expected
+to use, the product. A product is a consumer product regardless of
+whether the product has substantial commercial, industrial or
+non-consumer uses, unless such uses represent the only significant
+mode of use of the product.
+
+"Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to
+install and execute modified versions of a covered work in that User
+Product from a modified version of its Corresponding Source. The
+information must suffice to ensure that the continued functioning of
+the modified object code is in no case prevented or interfered with
+solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or
+updates for a work that has been modified or installed by the
+recipient, or for the User Product in which it has been modified or
+installed. Access to a network may be denied when the modification
+itself materially and adversely affects the operation of the network
+or violates the rules and protocols for communication across the
+network.
+
+Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+#### 7. Additional Terms.
+
+"Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders
+of that material) supplement the terms of this License with terms:
+
+- a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+- b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+- c) Prohibiting misrepresentation of the origin of that material,
+ or requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+- d) Limiting the use for publicity purposes of names of licensors
+ or authors of the material; or
+- e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+- f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions
+ of it) with contractual assumptions of liability to the recipient,
+ for any liability that these contractual assumptions directly
+ impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions; the
+above requirements apply either way.
+
+#### 8. Termination.
+
+You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+However, if you cease all violation of this License, then your license
+from a particular copyright holder is reinstated (a) provisionally,
+unless and until the copyright holder explicitly and finally
+terminates your license, and (b) permanently, if the copyright holder
+fails to notify you of the violation by some reasonable means prior to
+60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+#### 9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run
+a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+#### 10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+#### 11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims owned
+or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the
+scope of its coverage, prohibits the exercise of, or is conditioned on
+the non-exercise of one or more of the rights that are specifically
+granted under this License. You may not convey a covered work if you
+are a party to an arrangement with a third party that is in the
+business of distributing software, under which you make payment to the
+third party based on the extent of your activity of conveying the
+work, and under which the third party grants, to any of the parties
+who would receive the covered work from you, a discriminatory patent
+license (a) in connection with copies of the covered work conveyed by
+you (or copies made from those copies), or (b) primarily for and in
+connection with specific products or compilations that contain the
+covered work, unless you entered into that arrangement, or that patent
+license was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+#### 12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under
+this License and any other pertinent obligations, then as a
+consequence you may not convey it at all. For example, if you agree to
+terms that obligate you to collect a royalty for further conveying
+from those to whom you convey the Program, the only way you could
+satisfy both those terms and this License would be to refrain entirely
+from conveying the Program.
+
+#### 13. Use with the GNU Affero General Public License.
+
+Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+#### 14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions
+of the GNU General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in
+detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies that a certain numbered version of the GNU General Public
+License "or any later version" applies to it, you have the option of
+following the terms and conditions either of that numbered version or
+of any later version published by the Free Software Foundation. If the
+Program does not specify a version number of the GNU General Public
+License, you may choose any version ever published by the Free
+Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions
+of the GNU General Public License can be used, that proxy's public
+statement of acceptance of a version permanently authorizes you to
+choose that version for the Program.
+
+Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+#### 15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
+WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
+PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
+DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
+CORRECTION.
+
+#### 16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
+CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
+ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
+NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
+LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
+TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
+PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+#### 17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
diff --git a/frappe/core/doctype/package/licenses/MIT License.md b/frappe/core/doctype/package/licenses/MIT License.md
new file mode 100644
index 0000000000..c038ee76ae
--- /dev/null
+++ b/frappe/core/doctype/package/licenses/MIT License.md
@@ -0,0 +1,17 @@
+### MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this
+software and associated documentation files (the "Software"), to deal in the Software
+without restriction, including without limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies
+or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/frappe/core/doctype/package/package.js b/frappe/core/doctype/package/package.js
new file mode 100644
index 0000000000..90e2eed1e3
--- /dev/null
+++ b/frappe/core/doctype/package/package.js
@@ -0,0 +1,17 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Package', {
+ validate: function(frm) {
+ if (!frm.doc.package_name) {
+ frm.set_value('package_name', frm.doc.name.toLowerCase().replace(' ', '-'));
+ }
+ },
+
+ license_type: function(frm) {
+ frappe.call('frappe.core.doctype.package.package.get_license_text',
+ {'license_type': frm.doc.license_type}).then(r => {
+ frm.set_value('license', r.message);
+ });
+ }
+});
diff --git a/frappe/core/doctype/package/package.json b/frappe/core/doctype/package/package.json
new file mode 100644
index 0000000000..285e17a5bb
--- /dev/null
+++ b/frappe/core/doctype/package/package.json
@@ -0,0 +1,76 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "Prompt",
+ "creation": "2021-09-04 11:54:35.155687",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "package_name",
+ "readme",
+ "license_type",
+ "license"
+ ],
+ "fields": [
+ {
+ "fieldname": "readme",
+ "fieldtype": "Markdown Editor",
+ "label": "Readme"
+ },
+ {
+ "fieldname": "package_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Package Name",
+ "reqd": 1
+ },
+ {
+ "fieldname": "license_type",
+ "fieldtype": "Select",
+ "label": "License Type",
+ "options": "\nMIT License\nGNU General Public License\nGNU Affero General Public License"
+ },
+ {
+ "fieldname": "license",
+ "fieldtype": "Markdown Editor",
+ "label": "License"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [
+ {
+ "group": "Modules",
+ "link_doctype": "Module Def",
+ "link_fieldname": "package"
+ },
+ {
+ "group": "Release",
+ "link_doctype": "Package Release",
+ "link_fieldname": "package"
+ }
+ ],
+ "modified": "2021-09-05 13:15:01.130982",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Package",
+ "naming_rule": "Set by user",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/package/package.py b/frappe/core/doctype/package/package.py
new file mode 100644
index 0000000000..aa9735c061
--- /dev/null
+++ b/frappe/core/doctype/package/package.py
@@ -0,0 +1,18 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+import frappe
+import os
+from frappe.model.document import Document
+
+class Package(Document):
+ def validate(self):
+ if not self.package_name:
+ self.package_name = self.name.lower().replace(' ', '-')
+
+@frappe.whitelist()
+def get_license_text(license_type):
+ with open(os.path.join(os.path.dirname(__file__), 'licenses',
+ license_type + '.md'), 'r') as textfile:
+ return textfile.read()
+
diff --git a/frappe/core/doctype/package/test_package.py b/frappe/core/doctype/package/test_package.py
new file mode 100644
index 0000000000..3fb8d48274
--- /dev/null
+++ b/frappe/core/doctype/package/test_package.py
@@ -0,0 +1,89 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+
+import frappe
+import os
+import json
+import unittest
+
+class TestPackage(unittest.TestCase):
+ def test_package_release(self):
+ make_test_package()
+ make_test_module()
+ make_test_doctype()
+ make_test_server_script()
+ make_test_web_page()
+
+ # make release
+ frappe.get_doc(dict(
+ doctype = 'Package Release',
+ package = 'Test Package',
+ publish = 1
+ )).insert()
+
+ self.assertTrue(os.path.exists(frappe.get_site_path('packages', 'test-package')))
+ self.assertTrue(os.path.exists(frappe.get_site_path('packages', 'test-package', 'test_module_for_package')))
+ self.assertTrue(os.path.exists(frappe.get_site_path('packages', 'test-package', 'test_module_for_package', 'doctype', 'test_doctype_for_package')))
+ with open(frappe.get_site_path('packages', 'test-package', 'test_module_for_package',
+ 'doctype', 'test_doctype_for_package', 'test_doctype_for_package.json')) as f:
+ doctype = json.loads(f.read())
+ self.assertEqual(doctype['doctype'], 'DocType')
+ self.assertEqual(doctype['name'], 'Test DocType for Package')
+ self.assertEqual(doctype['fields'][0]['fieldname'], 'test_field')
+
+
+def make_test_package():
+ if not frappe.db.exists('Package', 'Test Package'):
+ frappe.get_doc(dict(
+ doctype = 'Package',
+ name = 'Test Package',
+ package_name = 'test-package',
+ readme = '# Test Package'
+ )).insert()
+
+def make_test_module():
+ if not frappe.db.exists('Module Def', 'Test Module for Package'):
+ frappe.get_doc(dict(
+ doctype = 'Module Def',
+ module_name = 'Test Module for Package',
+ custom = 1,
+ app_name = 'frappe',
+ package = 'Test Package'
+ )).insert()
+
+def make_test_doctype():
+ if not frappe.db.exists('DocType', 'Test DocType for Package'):
+ frappe.get_doc(dict(
+ doctype = 'DocType',
+ name = 'Test DocType for Package',
+ custom = 1,
+ module = 'Test Module for Package',
+ autoname = 'Prompt',
+ fields = [dict(
+ fieldname = 'test_field',
+ fieldtype = 'Data',
+ label = 'Test Field'
+ )]
+ )).insert()
+
+def make_test_server_script():
+ if not frappe.db.exists('Server Script', 'Test Script for Package'):
+ frappe.get_doc(dict(
+ doctype = 'Server Script',
+ name = 'Test Script for Package',
+ module = 'Test Module for Package',
+ script_type = 'DocType Event',
+ reference_doctype = 'Test DocType for Package',
+ doctype_event = 'Before Save',
+ script = 'frappe.msgprint("Test")'
+ )).insert()
+
+def make_test_web_page():
+ if not frappe.db.exists('Web Page', 'test-web-page-for-package'):
+ frappe.get_doc(dict(
+ doctype = "Web Page",
+ module = 'Test Module for Package',
+ main_section = "Some content",
+ published = 1,
+ title = "Test Web Page for Package"
+ )).insert()
diff --git a/frappe/chat/doctype/chat_profile/__init__.py b/frappe/core/doctype/package_import/__init__.py
similarity index 100%
rename from frappe/chat/doctype/chat_profile/__init__.py
rename to frappe/core/doctype/package_import/__init__.py
diff --git a/frappe/core/doctype/package_import/package_import.js b/frappe/core/doctype/package_import/package_import.js
new file mode 100644
index 0000000000..c01a6266cc
--- /dev/null
+++ b/frappe/core/doctype/package_import/package_import.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Package Import', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/core/doctype/package_import/package_import.json b/frappe/core/doctype/package_import/package_import.json
new file mode 100644
index 0000000000..f3c6168f8d
--- /dev/null
+++ b/frappe/core/doctype/package_import/package_import.json
@@ -0,0 +1,65 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "format:Package Import at {creation}",
+ "creation": "2021-09-05 16:36:46.680094",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "attach_package",
+ "activate",
+ "force",
+ "log"
+ ],
+ "fields": [
+ {
+ "fieldname": "attach_package",
+ "fieldtype": "Attach",
+ "label": "Attach Package"
+ },
+ {
+ "default": "0",
+ "fieldname": "activate",
+ "fieldtype": "Check",
+ "label": "Activate"
+ },
+ {
+ "fieldname": "log",
+ "fieldtype": "Code",
+ "label": "Log",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "force",
+ "fieldtype": "Check",
+ "label": "Force"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-09-05 21:30:04.796090",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Package Import",
+ "naming_rule": "Expression",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/package_import/package_import.py b/frappe/core/doctype/package_import/package_import.py
new file mode 100644
index 0000000000..f4a2d666dd
--- /dev/null
+++ b/frappe/core/doctype/package_import/package_import.py
@@ -0,0 +1,58 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+import frappe
+import os
+import json
+import subprocess
+from frappe.model.document import Document
+from frappe.desk.form.load import get_attachments
+from frappe.model.sync import get_doc_files
+from frappe.modules.import_file import import_file_by_path, import_doc
+
+class PackageImport(Document):
+ def validate(self):
+ if self.activate:
+ self.import_package()
+
+ def import_package(self):
+ attachment = get_attachments(self.doctype, self.name)
+
+ if not attachment:
+ frappe.throw(frappe._('Please attach the package'))
+
+ attachment = attachment[0]
+
+ # get package_name from file (package_name-0.0.0.tar.gz)
+ package_name = attachment.file_name.split('.')[0].rsplit('-', 1)[0]
+ if not os.path.exists(frappe.get_site_path('packages')):
+ os.makedirs(frappe.get_site_path('packages'))
+
+ # extract
+ subprocess.check_output(['tar', 'xzf',
+ frappe.get_site_path(attachment.file_url.strip('/')), '-C',
+ frappe.get_site_path('packages')])
+
+ package_path = frappe.get_site_path('packages', package_name)
+
+ # import Package
+ with open(os.path.join(package_path, package_name + '.json'), 'r') as packagefile:
+ doc_dict = json.loads(packagefile.read())
+
+ frappe.flags.package = import_doc(doc_dict)
+
+ # collect modules
+ files = []
+ log = []
+ for module in os.listdir(package_path):
+ module_path = os.path.join(package_path, module)
+ if os.path.isdir(module_path):
+ get_doc_files(files, module_path)
+
+ # import files
+ for file in files:
+ import_file_by_path(file, force=self.force, ignore_version=True,
+ for_sync=True)
+ log.append('Imported {}'.format(file))
+
+ self.log = '\n'.join(log)
diff --git a/frappe/core/doctype/package_import/test_package_import.py b/frappe/core/doctype/package_import/test_package_import.py
new file mode 100644
index 0000000000..04628fed93
--- /dev/null
+++ b/frappe/core/doctype/package_import/test_package_import.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+class TestPackageImport(unittest.TestCase):
+ pass
diff --git a/frappe/chat/doctype/chat_room/__init__.py b/frappe/core/doctype/package_release/__init__.py
similarity index 100%
rename from frappe/chat/doctype/chat_room/__init__.py
rename to frappe/core/doctype/package_release/__init__.py
diff --git a/frappe/core/doctype/package_release/package_release.js b/frappe/core/doctype/package_release/package_release.js
new file mode 100644
index 0000000000..9eabe36839
--- /dev/null
+++ b/frappe/core/doctype/package_release/package_release.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Package Release', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/core/doctype/package_release/package_release.json b/frappe/core/doctype/package_release/package_release.json
new file mode 100644
index 0000000000..b651d699c4
--- /dev/null
+++ b/frappe/core/doctype/package_release/package_release.json
@@ -0,0 +1,95 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2021-09-05 12:59:01.932327",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "package",
+ "publish",
+ "path",
+ "column_break_3",
+ "major",
+ "minor",
+ "patch",
+ "section_break_7",
+ "release_notes"
+ ],
+ "fields": [
+ {
+ "fieldname": "package",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Package",
+ "options": "Package",
+ "reqd": 1
+ },
+ {
+ "fieldname": "major",
+ "fieldtype": "Int",
+ "label": "Major"
+ },
+ {
+ "fieldname": "minor",
+ "fieldtype": "Int",
+ "label": "Minor"
+ },
+ {
+ "fieldname": "patch",
+ "fieldtype": "Int",
+ "label": "Patch",
+ "no_copy": 1
+ },
+ {
+ "fieldname": "path",
+ "fieldtype": "Small Text",
+ "label": "Path",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_7",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "release_notes",
+ "fieldtype": "Markdown Editor",
+ "label": "Release Notes"
+ },
+ {
+ "default": "0",
+ "fieldname": "publish",
+ "fieldtype": "Check",
+ "label": "Publish"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-09-05 16:04:32.860988",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Package Release",
+ "naming_rule": "By script",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/package_release/package_release.py b/frappe/core/doctype/package_release/package_release.py
new file mode 100644
index 0000000000..d23ae917c4
--- /dev/null
+++ b/frappe/core/doctype/package_release/package_release.py
@@ -0,0 +1,92 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.model.document import Document
+from frappe.modules.export_file import export_doc
+import os
+import subprocess
+from frappe.query_builder.functions import Max
+
+
+class PackageRelease(Document):
+ def set_version(self):
+ # set the next patch release by default
+ doctype = frappe.qb.DocType("Package Release")
+ if not self.major:
+ self.major = frappe.qb.from_(doctype) \
+ .where(doctype.package == self.package) \
+ .select(Max(doctype.minor)).run()[0][0] or 0
+
+ if not self.minor:
+ self.minor = frappe.qb.from_(doctype) \
+ .where(doctype.package == self.package) \
+ .select(Max("minor")).run()[0][0] or 0
+ if not self.patch:
+ value = frappe.qb.from_(doctype) \
+ .where(doctype.package == self.package) \
+ .select(Max("patch")).run()[0][0] or 0
+ self.patch = value + 1
+
+ def autoname(self):
+ self.set_version()
+ self.name = '{}-{}.{}.{}'.format(
+ frappe.db.get_value('Package', self.package, 'package_name'),
+ self.major, self.minor, self.patch)
+
+ def validate(self):
+ if self.publish:
+ self.export_files()
+
+ def export_files(self):
+ '''Export all the documents in this package to site/packages folder'''
+ package = frappe.get_doc('Package', self.package)
+
+ self.export_modules()
+ self.export_package_files(package)
+ self.make_tarfile(package)
+
+ def export_modules(self):
+ for m in frappe.db.get_all('Module Def', dict(package=self.package)):
+ module = frappe.get_doc('Module Def', m.name)
+ for l in module.meta.links:
+ if l.link_doctype == 'Module Def':
+ continue
+ # all documents of the type in the module
+ for d in frappe.get_all(l.link_doctype, dict(module=m.name)):
+ export_doc(frappe.get_doc(l.link_doctype, d.name))
+
+ def export_package_files(self, package):
+ # write readme
+ with open(frappe.get_site_path('packages', package.package_name, 'README.md'), 'w') as readme:
+ readme.write(package.readme)
+
+ # write license
+ if package.license:
+ with open(frappe.get_site_path('packages', package.package_name, 'LICENSE.md'), 'w') as license:
+ license.write(package.license)
+
+ # write package.json as `frappe_package.json`
+ with open(frappe.get_site_path('packages', package.package_name, package.package_name + '.json'), 'w') as packagefile:
+ packagefile.write(frappe.as_json(package.as_dict(no_nulls=True)))
+
+ def make_tarfile(self, package):
+ # make tarfile
+ filename = '{}.tar.gz'.format(self.name)
+ subprocess.check_output(['tar', 'czf', filename, package.package_name],
+ cwd=frappe.get_site_path('packages'))
+
+ # move file
+ subprocess.check_output(['mv', frappe.get_site_path('packages', filename),
+ frappe.get_site_path('public', 'files')])
+
+ # make attachment
+ file = frappe.get_doc(dict(
+ doctype = 'File',
+ file_url = '/' + os.path.join('files', filename),
+ attached_to_doctype = self.doctype,
+ attached_to_name = self.name
+ ))
+
+ file.flags.ignore_duplicate_entry_error = True
+ file.insert()
diff --git a/frappe/core/doctype/package_release/test_package_release.py b/frappe/core/doctype/package_release/test_package_release.py
new file mode 100644
index 0000000000..6a15e8625b
--- /dev/null
+++ b/frappe/core/doctype/package_release/test_package_release.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+class TestPackageRelease(unittest.TestCase):
+ pass
diff --git a/frappe/core/doctype/page/__init__.py b/frappe/core/doctype/page/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/core/doctype/page/__init__.py
+++ b/frappe/core/doctype/page/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/core/doctype/page/page.py b/frappe/core/doctype/page/page.py
index 0ba0e309dd..894e180bb1 100644
--- a/frappe/core/doctype/page/page.py
+++ b/frappe/core/doctype/page/page.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import os
@@ -109,6 +109,7 @@ class Page(Document):
if os.path.exists(fpath):
with open(fpath, 'r') as f:
self.script = render_include(f.read())
+ self.script += f"\n\n//# sourceURL={page_name}.js"
# css
fpath = os.path.join(path, page_name + '.css')
diff --git a/frappe/core/doctype/page/test_page.py b/frappe/core/doctype/page/test_page.py
index 18b4aea2c8..7db32497a8 100644
--- a/frappe/core/doctype/page/test_page.py
+++ b/frappe/core/doctype/page/test_page.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/patch_log/__init__.py b/frappe/core/doctype/patch_log/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/core/doctype/patch_log/__init__.py
+++ b/frappe/core/doctype/patch_log/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/core/doctype/patch_log/patch_log.py b/frappe/core/doctype/patch_log/patch_log.py
index cc66955eb8..9a5da24e37 100644
--- a/frappe/core/doctype/patch_log/patch_log.py
+++ b/frappe/core/doctype/patch_log/patch_log.py
@@ -1,7 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/core/doctype/patch_log/test_patch_log.py b/frappe/core/doctype/patch_log/test_patch_log.py
index d0690ecee0..df1ca16b22 100644
--- a/frappe/core/doctype/patch_log/test_patch_log.py
+++ b/frappe/core/doctype/patch_log/test_patch_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/payment_gateway/payment_gateway.py b/frappe/core/doctype/payment_gateway/payment_gateway.py
index 1459635b01..d0fa550ea1 100644
--- a/frappe/core/doctype/payment_gateway/payment_gateway.py
+++ b/frappe/core/doctype/payment_gateway/payment_gateway.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/payment_gateway/test_payment_gateway.py b/frappe/core/doctype/payment_gateway/test_payment_gateway.py
index 66f899bd27..e2ad081cfa 100644
--- a/frappe/core/doctype/payment_gateway/test_payment_gateway.py
+++ b/frappe/core/doctype/payment_gateway/test_payment_gateway.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py
index c68bb6a4f1..2d1b026572 100644
--- a/frappe/core/doctype/prepared_report/prepared_report.py
+++ b/frappe/core/doctype/prepared_report/prepared_report.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import json
@@ -11,8 +11,6 @@ from frappe.desk.query_report import generate_report_result
from frappe.model.document import Document
from frappe.utils import gzip_compress, gzip_decompress
from frappe.utils.background_jobs import enqueue
-from frappe.core.doctype.file.file import remove_all
-
class PreparedReport(Document):
def before_insert(self):
diff --git a/frappe/core/doctype/prepared_report/test_prepared_report.py b/frappe/core/doctype/prepared_report/test_prepared_report.py
index ef324dd01a..5b12990f64 100644
--- a/frappe/core/doctype/prepared_report/test_prepared_report.py
+++ b/frappe/core/doctype/prepared_report/test_prepared_report.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
import json
diff --git a/frappe/core/doctype/report/__init__.py b/frappe/core/doctype/report/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/core/doctype/report/__init__.py
+++ b/frappe/core/doctype/report/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/core/doctype/report/boilerplate/controller.py b/frappe/core/doctype/report/boilerplate/controller.py
index b8e9cb7467..ccf732a405 100644
--- a/frappe/core/doctype/report/boilerplate/controller.py
+++ b/frappe/core/doctype/report/boilerplate/controller.py
@@ -1,5 +1,5 @@
# Copyright (c) 2013, {app_publisher} and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py
index a5c61fa436..be0346d869 100644
--- a/frappe/core/doctype/report/report.py
+++ b/frappe/core/doctype/report/report.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import json, datetime
from frappe import _, scrub
@@ -105,7 +105,7 @@ class Report(Document):
if not self.query.lower().startswith("select"):
frappe.throw(_("Query must be a SELECT"), title=_('Report Document Error'))
- result = [list(t) for t in frappe.db.sql(self.query, filters, debug=True)]
+ result = [list(t) for t in frappe.db.sql(self.query, filters)]
columns = self.get_columns() or [cstr(c[0]) for c in frappe.db.get_description()]
return [columns, result]
diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py
index 9d0c0b9af0..36e3b09254 100644
--- a/frappe/core/doctype/report/test_report.py
+++ b/frappe/core/doctype/report/test_report.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe, json, os
import unittest
@@ -82,9 +82,11 @@ class TestReport(unittest.TestCase):
def test_report_permissions(self):
frappe.set_user('test@example.com')
- frappe.db.sql("""delete from `tabHas Role` where parent = %s
- and role = 'Test Has Role'""", frappe.session.user, auto_commit=1)
-
+ frappe.db.delete("Has Role", {
+ "parent": frappe.session.user,
+ "role": "Test Has Role"
+ })
+ frappe.db.commit()
if not frappe.db.exists('Role', 'Test Has Role'):
role = frappe.get_doc({
'doctype': 'Role',
diff --git a/frappe/core/doctype/report_column/report_column.py b/frappe/core/doctype/report_column/report_column.py
index f9078d820d..3b2c1e130b 100644
--- a/frappe/core/doctype/report_column/report_column.py
+++ b/frappe/core/doctype/report_column/report_column.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/report_filter/report_filter.py b/frappe/core/doctype/report_filter/report_filter.py
index ccdcc0eb6f..b325985308 100644
--- a/frappe/core/doctype/report_filter/report_filter.py
+++ b/frappe/core/doctype/report_filter/report_filter.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/role/__init__.py b/frappe/core/doctype/role/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/core/doctype/role/__init__.py
+++ b/frappe/core/doctype/role/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/core/doctype/role/patches/v13_set_default_desk_properties.py b/frappe/core/doctype/role/patches/v13_set_default_desk_properties.py
index 375ea02e0e..dc17526047 100644
--- a/frappe/core/doctype/role/patches/v13_set_default_desk_properties.py
+++ b/frappe/core/doctype/role/patches/v13_set_default_desk_properties.py
@@ -2,9 +2,10 @@ import frappe
from ..role import desk_properties
def execute():
+ frappe.reload_doctype('user')
frappe.reload_doctype('role')
for role in frappe.get_all('Role', ['name', 'desk_access']):
role_doc = frappe.get_doc('Role', role.name)
for key in desk_properties:
role_doc.set(key, role_doc.desk_access)
- role_doc.save()
\ No newline at end of file
+ role_doc.save()
diff --git a/frappe/core/doctype/role/role.json b/frappe/core/doctype/role/role.json
index 0135cbf9e8..ba82e023a9 100644
--- a/frappe/core/doctype/role/role.json
+++ b/frappe/core/doctype/role/role.json
@@ -17,7 +17,6 @@
"navigation_settings_section",
"search_bar",
"notifications",
- "chat",
"list_settings_section",
"list_sidebar",
"bulk_actions",
@@ -85,12 +84,6 @@
"fieldtype": "Check",
"label": "Search Bar"
},
- {
- "default": "1",
- "fieldname": "chat",
- "fieldtype": "Check",
- "label": "Chat"
- },
{
"fieldname": "list_settings_section",
"fieldtype": "Section Break",
@@ -155,10 +148,11 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-01-27 10:35:37.638350",
+ "modified": "2021-10-08 14:06:55.729364",
"modified_by": "Administrator",
"module": "Core",
"name": "Role",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py
index 02482c75ca..98d2d72fc2 100644
--- a/frappe/core/doctype/role/role.py
+++ b/frappe/core/doctype/role/role.py
@@ -1,11 +1,11 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
-desk_properties = ("search_bar", "notifications", "chat", "list_sidebar",
+desk_properties = ("search_bar", "notifications", "list_sidebar",
"bulk_actions", "view_switcher", "form_sidebar", "timeline", "dashboard")
class Role(Document):
@@ -38,7 +38,7 @@ class Role(Document):
self.set(key, 0)
def remove_roles(self):
- frappe.db.sql("delete from `tabHas Role` where role = %s", self.name)
+ frappe.db.delete("Has Role", {"role": self.name})
frappe.clear_cache()
def on_update(self):
@@ -82,4 +82,4 @@ def role_query(doctype, txt, searchfield, start, page_len, filters):
report_filters.extend(filters)
return frappe.get_all('Role', limit_start=start, limit_page_length=page_len,
- filters=report_filters, as_list=1)
\ No newline at end of file
+ filters=report_filters, as_list=1)
diff --git a/frappe/core/doctype/role/test_role.py b/frappe/core/doctype/role/test_role.py
index 471f6cac43..1671f9a9c8 100644
--- a/frappe/core/doctype/role/test_role.py
+++ b/frappe/core/doctype/role/test_role.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py b/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py
index 59f34a1483..cd9a6dc0fa 100644
--- a/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py
+++ b/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.core.doctype.report.report import is_prepared_report_disabled
diff --git a/frappe/core/doctype/role_profile/role_profile.py b/frappe/core/doctype/role_profile/role_profile.py
index 0f58da5b5e..cb0a43d68f 100644
--- a/frappe/core/doctype/role_profile/role_profile.py
+++ b/frappe/core/doctype/role_profile/role_profile.py
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from frappe.model.document import Document
+import frappe
class RoleProfile(Document):
def autoname(self):
@@ -11,5 +12,9 @@ class RoleProfile(Document):
def on_update(self):
""" Changes in role_profile reflected across all its user """
- from frappe.core.doctype.user.user import update_roles
- update_roles(self.name)
+ users = frappe.get_all('User', filters={'role_profile_name': self.name})
+ roles = [role.role for role in self.roles]
+ for d in users:
+ user = frappe.get_doc('User', d)
+ user.set('roles', [])
+ user.add_roles(*roles)
diff --git a/frappe/core/doctype/role_profile/test_role_profile.py b/frappe/core/doctype/role_profile/test_role_profile.py
index 53e0a1b043..b208a186de 100644
--- a/frappe/core/doctype/role_profile/test_role_profile.py
+++ b/frappe/core/doctype/role_profile/test_role_profile.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
@@ -8,6 +8,7 @@ test_dependencies = ['Role']
class TestRoleProfile(unittest.TestCase):
def test_make_new_role_profile(self):
+ frappe.delete_doc_if_exists('Role Profile', 'Test 1', force=1)
new_role_profile = frappe.get_doc(dict(doctype='Role Profile', role_profile='Test 1')).insert()
self.assertEqual(new_role_profile.role_profile, 'Test 1')
@@ -19,7 +20,25 @@ class TestRoleProfile(unittest.TestCase):
new_role_profile.save()
self.assertEqual(new_role_profile.roles[0].role, '_Test Role 2')
+ # user with a role profile
+ random_user = frappe.mock("email")
+ random_user_name = frappe.mock("name")
+
+ random_user = frappe.get_doc({
+ "doctype": "User",
+ "email": random_user,
+ "enabled": 1,
+ "first_name": random_user_name,
+ "new_password": "Eastern_43A1W",
+ "role_profile_name": 'Test 1'
+ }).insert(ignore_permissions=True, ignore_if_duplicate=True)
+ self.assertListEqual([role.role for role in random_user.roles], [role.role for role in new_role_profile.roles])
+
# clear roles
new_role_profile.roles = []
new_role_profile.save()
self.assertEqual(new_role_profile.roles, [])
+
+ # user roles with the role profile should also be updated
+ random_user.reload()
+ self.assertListEqual(random_user.roles, [])
\ No newline at end of file
diff --git a/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json
index f86a4c8884..396b32bdf9 100644
--- a/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json
+++ b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json
@@ -38,7 +38,7 @@
}
],
"links": [],
- "modified": "2020-01-22 00:00:00.000000",
+ "modified": "2021-10-25 00:00:00.000000",
"modified_by": "Administrator",
"module": "Core",
"name": "Scheduled Job Log",
@@ -59,6 +59,5 @@
],
"quick_entry": 1,
"sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
+ "sort_order": "DESC"
}
diff --git a/frappe/core/doctype/scheduled_job_log/scheduled_job_log.py b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.py
index 7f54a3b6ae..bd5c15bc31 100644
--- a/frappe/core/doctype/scheduled_job_log/scheduled_job_log.py
+++ b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/scheduled_job_log/test_scheduled_job_log.py b/frappe/core/doctype/scheduled_job_log/test_scheduled_job_log.py
index 85471d0d71..9957f6c34c 100644
--- a/frappe/core/doctype/scheduled_job_log/test_scheduled_job_log.py
+++ b/frappe/core/doctype/scheduled_job_log/test_scheduled_job_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
index 59089d12ad..1a795bab82 100644
--- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
+++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
@@ -1,6 +1,5 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# Copyright (c) 2021, Frappe Technologies and contributors
+# License: MIT. See LICENSE
import json
from datetime import datetime
@@ -110,7 +109,7 @@ class ScheduledJobType(Document):
return 'long' if ('Long' in self.frequency) else 'default'
def on_trash(self):
- frappe.db.sql('delete from `tabScheduled Job Log` where scheduled_job_type=%s', self.name)
+ frappe.db.delete("Scheduled Job Log", {"scheduled_job_type": self.name})
@frappe.whitelist()
diff --git a/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py
index a071cfe9a9..dc3353b176 100644
--- a/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py
+++ b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
from frappe.utils import get_datetime
diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json
index b7e49673f8..520c0008c5 100644
--- a/frappe/core/doctype/server_script/server_script.json
+++ b/frappe/core/doctype/server_script/server_script.json
@@ -13,6 +13,7 @@
"api_method",
"allow_guest",
"column_break_3",
+ "module",
"disabled",
"section_break_8",
"script",
@@ -93,6 +94,12 @@
"label": "Event Frequency",
"mandatory_depends_on": "eval:doc.script_type == \"Scheduler Event\"",
"options": "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long"
+ },
+ {
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module (for export)",
+ "options": "Module Def"
}
],
"index_web_pages_for_search": 1,
@@ -102,7 +109,7 @@
"link_fieldname": "server_script"
}
],
- "modified": "2021-02-18 12:36:19.803425",
+ "modified": "2021-09-04 12:02:43.671240",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",
diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py
index d26fe5a188..5b1aab1241 100644
--- a/frappe/core/doctype/server_script/server_script.py
+++ b/frappe/core/doctype/server_script/server_script.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import ast
from types import FunctionType, MethodType, ModuleType
@@ -15,7 +15,6 @@ from frappe import _
class ServerScript(Document):
def validate(self):
frappe.only_for("Script Manager", True)
- self.validate_script()
self.sync_scheduled_jobs()
self.clear_scheduled_events()
@@ -28,6 +27,11 @@ class ServerScript(Document):
for job in self.scheduled_jobs:
frappe.delete_doc("Scheduled Job Type", job.name)
+ def get_code_fields(self):
+ return {
+ 'script': 'py'
+ }
+
@property
def scheduled_jobs(self) -> List[Dict[str, str]]:
return frappe.get_all(
@@ -36,10 +40,6 @@ class ServerScript(Document):
fields=["name", "stopped"],
)
- def validate_script(self):
- """Utilizes the ast module to check for syntax errors
- """
- ast.parse(self.script)
def sync_scheduled_jobs(self):
"""Sync Scheduled Job Type statuses if Server Script's disabled status is changed
@@ -94,7 +94,7 @@ class ServerScript(Document):
Args:
doc (Document): Executes script with for a certain document's events
"""
- safe_exec(self.script, _locals={"doc": doc})
+ safe_exec(self.script, _locals={"doc": doc}, restrict_commit_rollback=True)
def execute_scheduled_method(self):
"""Specific to Scheduled Jobs via Server Scripts
diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py
index c39fcfa0d0..3c091fec0b 100644
--- a/frappe/core/doctype/server_script/test_server_script.py
+++ b/frappe/core/doctype/server_script/test_server_script.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
import requests
@@ -59,6 +59,16 @@ conditions = '1 = 1'
reference_doctype = 'Note',
script = '''
frappe.method_that_doesnt_exist("do some magic")
+'''
+ ),
+ dict(
+ name='test_todo_commit',
+ script_type = 'DocType Event',
+ doctype_event = 'Before Save',
+ reference_doctype = 'ToDo',
+ disabled = 1,
+ script = '''
+frappe.db.commit()
'''
)
]
@@ -102,10 +112,30 @@ class TestServerScript(unittest.TestCase):
self.assertEqual(frappe.get_doc('Server Script', 'test_return_value').execute_method(), 'hello')
def test_permission_query(self):
- self.assertTrue('where (1 = 1)' in frappe.db.get_list('ToDo', return_query=1))
+ self.assertTrue('where (1 = 1)' in frappe.db.get_list('ToDo', run=False))
self.assertTrue(isinstance(frappe.db.get_list('ToDo'), list))
def test_attribute_error(self):
"""Raise AttributeError if method not found in Namespace"""
note = frappe.get_doc({"doctype": "Note", "title": "Test Note: Server Script"})
self.assertRaises(AttributeError, note.insert)
+
+ def test_syntax_validation(self):
+ server_script = scripts[0]
+ server_script["script"] = "js || code.?"
+
+ with self.assertRaises(frappe.ValidationError) as se:
+ frappe.get_doc(doctype="Server Script", **server_script).insert()
+
+ self.assertTrue("invalid python code" in str(se.exception).lower(),
+ msg="Python code validation not working")
+
+ def test_commit_in_doctype_event(self):
+ server_script = frappe.get_doc('Server Script', 'test_todo_commit')
+ server_script.disabled = 0
+ server_script.save()
+
+ self.assertRaises(AttributeError, frappe.get_doc(dict(doctype='ToDo', description='test me')).insert)
+
+ server_script.disabled = 1
+ server_script.save()
diff --git a/frappe/core/doctype/session_default/session_default.py b/frappe/core/doctype/session_default/session_default.py
index 70ff103111..9470a1bb38 100644
--- a/frappe/core/doctype/session_default/session_default.py
+++ b/frappe/core/doctype/session_default/session_default.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/session_default_settings/session_default_settings.py b/frappe/core/doctype/session_default_settings/session_default_settings.py
index 25f7522c86..52c917223e 100644
--- a/frappe/core/doctype/session_default_settings/session_default_settings.py
+++ b/frappe/core/doctype/session_default_settings/session_default_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
diff --git a/frappe/core/doctype/session_default_settings/test_session_default_settings.py b/frappe/core/doctype/session_default_settings/test_session_default_settings.py
index 7d20015b66..7a7e971aed 100644
--- a/frappe/core/doctype/session_default_settings/test_session_default_settings.py
+++ b/frappe/core/doctype/session_default_settings/test_session_default_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
from frappe.core.doctype.session_default_settings.session_default_settings import set_session_default_values, clear_session_defaults
diff --git a/frappe/core/doctype/sms_parameter/sms_parameter.py b/frappe/core/doctype/sms_parameter/sms_parameter.py
index d1fb1c53db..fb8466eac6 100644
--- a/frappe/core/doctype/sms_parameter/sms_parameter.py
+++ b/frappe/core/doctype/sms_parameter/sms_parameter.py
@@ -1,5 +1,5 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/core/doctype/sms_settings/sms_settings.json b/frappe/core/doctype/sms_settings/sms_settings.json
index 073fb88bc7..d29949af45 100755
--- a/frappe/core/doctype/sms_settings/sms_settings.json
+++ b/frappe/core/doctype/sms_settings/sms_settings.json
@@ -1,238 +1,80 @@
{
- "allow_copy": 1,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2013-01-10 16:34:24",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "editable_grid": 0,
+ "actions": [],
+ "allow_copy": 1,
+ "creation": "2013-01-10 16:34:24",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "sms_gateway_url",
+ "message_parameter",
+ "receiver_parameter",
+ "static_parameters_section",
+ "parameters",
+ "use_post"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "Eg. smsgateway.com/api/send_sms.cgi",
- "fieldname": "sms_gateway_url",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "SMS Gateway URL",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "description": "Eg. smsgateway.com/api/send_sms.cgi",
+ "fieldname": "sms_gateway_url",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "SMS Gateway URL",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "Enter url parameter for message",
- "fieldname": "message_parameter",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Message Parameter",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "description": "Enter url parameter for message",
+ "fieldname": "message_parameter",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Message Parameter",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "Enter url parameter for receiver nos",
- "fieldname": "receiver_parameter",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Receiver Parameter",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "description": "Enter url parameter for receiver nos",
+ "fieldname": "receiver_parameter",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Receiver Parameter",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "static_parameters_section",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "fieldname": "static_parameters_section",
+ "fieldtype": "Column Break",
"width": "50%"
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "Enter static url parameters here (Eg. sender=ERPNext, username=ERPNext, password=1234 etc.)",
- "fieldname": "parameters",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Static Parameters",
- "length": 0,
- "no_copy": 0,
- "options": "SMS Parameter",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "description": "Enter static url parameters here (Eg. sender=ERPNext, username=ERPNext, password=1234 etc.)",
+ "fieldname": "parameters",
+ "fieldtype": "Table",
+ "label": "Static Parameters",
+ "options": "SMS Parameter"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "use_post",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Use POST",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "default": "0",
+ "fieldname": "use_post",
+ "fieldtype": "Check",
+ "label": "Use POST"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-cog",
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 1,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2021-03-02 18:06:00.868688",
- "modified_by": "Administrator",
- "module": "Core",
- "name": "SMS Settings",
- "owner": "Administrator",
+ ],
+ "icon": "fa fa-cog",
+ "idx": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2021-09-21 19:45:26.809793",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "SMS Settings",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 0,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "track_changes": 1,
- "track_seen": 0
-}
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/sms_settings/sms_settings.py b/frappe/core/doctype/sms_settings/sms_settings.py
index 58a0ff08f6..f15ba7e4f6 100644
--- a/frappe/core/doctype/sms_settings/sms_settings.py
+++ b/frappe/core/doctype/sms_settings/sms_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/core/doctype/sms_settings/test_sms_settings.py b/frappe/core/doctype/sms_settings/test_sms_settings.py
index 862f5e3965..b3be912f9e 100644
--- a/frappe/core/doctype/sms_settings/test_sms_settings.py
+++ b/frappe/core/doctype/sms_settings/test_sms_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/success_action/success_action.py b/frappe/core/doctype/success_action/success_action.py
index 4ebd3d250b..afb3a87485 100644
--- a/frappe/core/doctype/success_action/success_action.py
+++ b/frappe/core/doctype/success_action/success_action.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from frappe.model.document import Document
diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index 4b53983702..82e88d2477 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -23,6 +23,7 @@
"currency_precision",
"sec_backup_limit",
"backup_limit",
+ "encrypt_backup",
"background_workers",
"enable_scheduler",
"dormant_days",
@@ -65,9 +66,7 @@
"attach_view_link",
"prepared_report_section",
"enable_prepared_report_auto_deletion",
- "prepared_report_expiry_period",
- "chat",
- "enable_chat"
+ "prepared_report_expiry_period"
],
"fields": [
{
@@ -381,18 +380,6 @@
"fieldtype": "Check",
"label": "Hide footer in auto email reports"
},
- {
- "collapsible": 1,
- "fieldname": "chat",
- "fieldtype": "Section Break",
- "label": "Chat"
- },
- {
- "default": "1",
- "fieldname": "enable_chat",
- "fieldtype": "Check",
- "label": "Enable Chat"
- },
{
"fieldname": "column_break_21",
"fieldtype": "Column Break"
@@ -469,12 +456,18 @@
"fieldname": "strip_exif_metadata_from_uploaded_images",
"fieldtype": "Check",
"label": "Strip EXIF tags from uploaded images"
+ },
+ {
+ "default": "0",
+ "fieldname": "encrypt_backup",
+ "fieldtype": "Check",
+ "label": "Encrypt Backups"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
- "modified": "2021-03-30 11:47:47.330437",
+ "modified": "2021-10-21 19:24:15.232430",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
@@ -492,4 +485,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py
index 466914569f..1ae8e9e79e 100644
--- a/frappe/core/doctype/system_settings/system_settings.py
+++ b/frappe/core/doctype/system_settings/system_settings.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
diff --git a/frappe/core/doctype/system_settings/test_system_settings.py b/frappe/core/doctype/system_settings/test_system_settings.py
index a65c602abe..f95e26b793 100644
--- a/frappe/core/doctype/system_settings/test_system_settings.py
+++ b/frappe/core/doctype/system_settings/test_system_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/test/test.py b/frappe/core/doctype/test/test.py
index 98e36e6a30..4cb088c117 100644
--- a/frappe/core/doctype/test/test.py
+++ b/frappe/core/doctype/test/test.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/test/test_test.py b/frappe/core/doctype/test/test_test.py
index d8ca975d63..d8508b8651 100644
--- a/frappe/core/doctype/test/test_test.py
+++ b/frappe/core/doctype/test/test_test.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/core/doctype/transaction_log/test_transaction_log.py b/frappe/core/doctype/transaction_log/test_transaction_log.py
index 0d9b9353d0..c332a82f65 100644
--- a/frappe/core/doctype/transaction_log/test_transaction_log.py
+++ b/frappe/core/doctype/transaction_log/test_transaction_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
import hashlib
diff --git a/frappe/core/doctype/transaction_log/transaction_log.py b/frappe/core/doctype/transaction_log/transaction_log.py
index 58d0b3d176..6dc4340277 100644
--- a/frappe/core/doctype/transaction_log/transaction_log.py
+++ b/frappe/core/doctype/transaction_log/transaction_log.py
@@ -1,12 +1,13 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
+# Copyright (c) 2021, Frappe Technologies and contributors
+# License: MIT. See LICENSE
+
+import hashlib
import frappe
from frappe import _
from frappe.model.document import Document
+from frappe.query_builder import DocType
from frappe.utils import cint, now_datetime
-import hashlib
class TransactionLog(Document):
def before_insert(self):
@@ -14,10 +15,9 @@ class TransactionLog(Document):
self.row_index = index
self.timestamp = now_datetime()
if index != 1:
- prev_hash = frappe.db.sql(
- "SELECT `chaining_hash` FROM `tabTransaction Log` WHERE `row_index` = '{0}'".format(index - 1))
+ prev_hash = frappe.get_all("Transaction Log", filters={"row_index":str(index-1)}, pluck="chaining_hash", limit=1)
if prev_hash:
- self.previous_hash = prev_hash[0][0]
+ self.previous_hash = prev_hash[0]
else:
self.previous_hash = "Indexing broken"
else:
@@ -45,10 +45,14 @@ class TransactionLog(Document):
def get_current_index():
- current = frappe.db.sql("""SELECT `current`
- FROM `tabSeries`
- WHERE `name` = 'TRANSACTLOG'
- FOR UPDATE""")
+ series = DocType("Series")
+ current = (
+ frappe.qb.from_(series)
+ .where(series.name == "TRANSACTLOG")
+ .for_update()
+ .select("current")
+ ).run()
+
if current and current[0][0] is not None:
current = current[0][0]
diff --git a/frappe/core/doctype/translation/test_translation.py b/frappe/core/doctype/translation/test_translation.py
index ae1293b38f..982d9bf976 100644
--- a/frappe/core/doctype/translation/test_translation.py
+++ b/frappe/core/doctype/translation/test_translation.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
@@ -8,7 +8,7 @@ from frappe import _
class TestTranslation(unittest.TestCase):
def setUp(self):
- frappe.db.sql('delete from tabTranslation')
+ frappe.db.delete("Translation")
def tearDown(self):
frappe.local.lang = 'en'
diff --git a/frappe/core/doctype/translation/translation.py b/frappe/core/doctype/translation/translation.py
index b1f4642791..a01552903c 100644
--- a/frappe/core/doctype/translation/translation.py
+++ b/frappe/core/doctype/translation/translation.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/user/test_records.json b/frappe/core/doctype/user/test_records.json
index f9033d4660..21fe3ff69d 100644
--- a/frappe/core/doctype/user/test_records.json
+++ b/frappe/core/doctype/user/test_records.json
@@ -70,5 +70,19 @@
"role": "System Manager"
}
]
- }
+ },
+ {
+ "doctype": "User",
+ "email": "testpassword@example.com",
+ "enabled": 1,
+ "first_name": "_Test",
+ "new_password": "Eastern_43A1W",
+ "roles": [
+ {
+ "doctype": "Has Role",
+ "parentfield": "roles",
+ "role": "System Manager"
+ }
+ ]
+ }
]
diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py
index 392128834d..e47846958a 100644
--- a/frappe/core/doctype/user/test_user.py
+++ b/frappe/core/doctype/user/test_user.py
@@ -1,16 +1,18 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-import frappe, unittest, uuid
+# License: MIT. See LICENSE
+import json
+import unittest
+from unittest.mock import patch
-from frappe.model.delete_doc import delete_doc
-from frappe.utils.data import today, add_to_date
-from frappe import _dict
-from frappe.utils import get_url
-from frappe.core.doctype.user.user import get_total_users
-from frappe.core.doctype.user.user import MaxUsersReachedError, test_password_strength
-from frappe.core.doctype.user.user import extract_mentions
+import frappe
+import frappe.exceptions
+from frappe.core.doctype.user.user import (extract_mentions, reset_password,
+ sign_up, test_password_strength, update_password, verify_password)
from frappe.frappeclient import FrappeClient
+from frappe.model.delete_doc import delete_doc
+from frappe.utils import get_url
+user_module = frappe.core.doctype.user.user
test_records = frappe.get_test_records('User')
class TestUser(unittest.TestCase):
@@ -23,7 +25,7 @@ class TestUser(unittest.TestCase):
def test_user_type(self):
new_user = frappe.get_doc(dict(doctype='User', email='test-for-type@example.com',
- first_name='Tester')).insert()
+ first_name='Tester')).insert(ignore_if_duplicate=True)
self.assertEqual(new_user.user_type, 'Website User')
# social login userid for frappe
@@ -52,7 +54,7 @@ class TestUser(unittest.TestCase):
def test_delete(self):
frappe.get_doc("User", "test@example.com").add_roles("_Test Role 2")
self.assertRaises(frappe.LinkExistsError, delete_doc, "Role", "_Test Role 2")
- frappe.db.sql("""delete from `tabHas Role` where role='_Test Role 2'""")
+ frappe.db.delete("Has Role", {"role": "_Test Role 2"})
delete_doc("Role","_Test Role 2")
if frappe.db.exists("User", "_test@example.com"):
@@ -119,40 +121,9 @@ class TestUser(unittest.TestCase):
# system manager now added by Administrator
self.assertTrue("System Manager" in [d.role for d in me.get("roles")])
- # def test_deny_multiple_sessions(self):
- # from frappe.installer import update_site_config
- # clear_limit('users')
- #
- # # allow one session
- # user = frappe.get_doc('User', 'test@example.com')
- # user.simultaneous_sessions = 1
- # user.new_password = 'Eastern_43A1W'
- # user.save()
- #
- # def test_request(conn):
- # value = conn.get_value('User', 'first_name', {'name': 'test@example.com'})
- # self.assertTrue('first_name' in value)
- #
- # from frappe.frappeclient import FrappeClient
- # update_site_config('deny_multiple_sessions', 0)
- #
- # conn1 = FrappeClient(get_url(), "test@example.com", "Eastern_43A1W", verify=False)
- # test_request(conn1)
- #
- # conn2 = FrappeClient(get_url(), "test@example.com", "Eastern_43A1W", verify=False)
- # test_request(conn2)
- #
- # update_site_config('deny_multiple_sessions', 1)
- # conn3 = FrappeClient(get_url(), "test@example.com", "Eastern_43A1W", verify=False)
- # test_request(conn3)
- #
- # # first connection should fail
- # test_request(conn1)
-
-
def test_delete_user(self):
new_user = frappe.get_doc(dict(doctype='User', email='test-for-delete@example.com',
- first_name='Tester Delete User')).insert()
+ first_name='Tester Delete User')).insert(ignore_if_duplicate=True)
self.assertEqual(new_user.user_type, 'Website User')
# role with desk access
@@ -174,7 +145,7 @@ class TestUser(unittest.TestCase):
self.assertFalse(frappe.db.exists('User', new_user.name))
def test_password_strength(self):
- # Test Password without Password Strenth Policy
+ # Test Password without Password Strength Policy
frappe.db.set_value("System Settings", "System Settings", "enable_password_policy", 0)
# password policy is disabled, test_password_strength should be ignored
@@ -193,6 +164,17 @@ class TestUser(unittest.TestCase):
result = test_password_strength("Eastern_43A1W")
self.assertEqual(result['feedback']['password_policy_validation_passed'], True)
+
+ # test password strength while saving user with new password
+ user = frappe.get_doc("User", "test@example.com")
+ frappe.flags.in_test = False
+ user.new_password = "password"
+ self.assertRaisesRegex(frappe.exceptions.ValidationError, "Invalid Password", user.save)
+ user.reload()
+ user.new_password = "Eastern_43A1W"
+ user.save()
+ frappe.flags.in_test = True
+
def test_comment_mentions(self):
comment = '''
@@ -227,6 +209,7 @@ class TestUser(unittest.TestCase):
self.assertEqual(extract_mentions(comment)[0], "test_user@example.com")
self.assertEqual(extract_mentions(comment)[1], "test.again@example1.com")
+ frappe.delete_doc("User Group", "Team")
doc = frappe.get_doc({
'doctype': 'User Group',
'name': 'Team',
@@ -236,14 +219,18 @@ class TestUser(unittest.TestCase):
'user': 'test1@example.com'
}]
})
- doc.insert(ignore_if_duplicate=True)
+
+ doc.insert()
comment = '''
Testing comment for
@Team
-
+ and
+
+ @Unknown Team
+
please check
'''
@@ -267,32 +254,125 @@ class TestUser(unittest.TestCase):
self.assertEqual(res1.status_code, 200)
self.assertEqual(res2.status_code, 417)
- # def test_user_rollback(self):
- # """
- # FIXME: This is failing with PR #12693 as Rollback can't happen if notifications sent on user creation.
- # Make sure that notifications disabled.
- # """
- # frappe.db.commit()
- # frappe.db.begin()
- # user_id = str(uuid.uuid4())
- # email = f'{user_id}@example.com'
- # try:
- # frappe.flags.in_import = True # disable throttling
- # frappe.get_doc(dict(
- # doctype='User',
- # email=email,
- # first_name=user_id,
- # )).insert()
- # finally:
- # frappe.flags.in_import = False
+ def test_user_rename(self):
+ old_name = "test_user_rename@example.com"
+ new_name = "test_user_rename_new@example.com"
+ user = frappe.get_doc({
+ "doctype": "User",
+ "email": old_name,
+ "enabled": 1,
+ "first_name": "_Test",
+ "new_password": "Eastern_43A1W",
+ "roles": [
+ {
+ "doctype": "Has Role",
+ "parentfield": "roles",
+ "role": "System Manager"
+ }]
+ }).insert(ignore_permissions=True, ignore_if_duplicate=True)
- # # Check user has been added
- # self.assertIsNotNone(frappe.db.get("User", {"email": email}))
+ frappe.rename_doc('User', user.name, new_name)
+ self.assertTrue(frappe.db.exists("Notification Settings", new_name))
+
+ frappe.delete_doc("User", new_name)
+
+ def test_signup(self):
+ import frappe.website.utils
+ random_user = frappe.mock('email')
+ random_user_name = frappe.mock('name')
+ # disabled signup
+ with patch.object(user_module, "is_signup_disabled", return_value=True):
+ self.assertRaisesRegex(frappe.exceptions.ValidationError, "Sign Up is disabled",
+ sign_up, random_user, random_user_name, "/signup")
+
+ self.assertTupleEqual(sign_up(random_user, random_user_name, "/welcome"), (1, "Please check your email for verification"))
+ self.assertEqual(frappe.cache().hget('redirect_after_login', random_user), "/welcome")
+
+ # re-register
+ self.assertTupleEqual(sign_up(random_user, random_user_name, "/welcome"), (0, "Already Registered"))
+
+ # disabled user
+ user = frappe.get_doc("User", random_user)
+ user.enabled = 0
+ user.save()
+
+ self.assertTupleEqual(sign_up(random_user, random_user_name, "/welcome"), (0, "Registered but disabled"))
+
+ # throttle user creation
+ with patch.object(user_module.frappe.db, "get_creation_count", return_value=301):
+ self.assertRaisesRegex(frappe.exceptions.ValidationError, "Throttled",
+ sign_up, frappe.mock('email'), random_user_name, "/signup")
+
+
+ def test_reset_password(self):
+ from frappe.auth import CookieManager, LoginManager
+ from frappe.utils import set_request
+ old_password = "Eastern_43A1W"
+ new_password = "easy_password"
+
+ set_request(path="/random")
+ frappe.local.cookie_manager = CookieManager()
+ frappe.local.login_manager = LoginManager()
+
+ frappe.set_user("testpassword@example.com")
+ test_user = frappe.get_doc("User", "testpassword@example.com")
+ test_user.reset_password()
+ self.assertEqual(update_password(new_password, key=test_user.reset_password_key), "/app")
+ self.assertEqual(update_password(new_password, key="wrong_key"), "The Link specified has either been used before or Invalid")
+
+ # password verification should fail with old password
+ self.assertRaises(frappe.exceptions.AuthenticationError, verify_password, old_password)
+ verify_password(new_password)
+
+ # reset password
+ update_password(old_password, old_password=new_password)
+
+ self.assertRaisesRegex(frappe.exceptions.ValidationError, "Invalid key type", update_password, "test", 1, ['like', '%'])
+
+ password_strength_response = {
+ "feedback": {
+ "password_policy_validation_passed": False,
+ "suggestions": ["Fix password"]
+ }
+ }
+
+ # password strength failure test
+ with patch.object(user_module, "test_password_strength", return_value=password_strength_response):
+ self.assertRaisesRegex(frappe.exceptions.ValidationError, "Fix password", update_password, new_password, 0, test_user.reset_password_key)
+
+
+ # test redirect URL for website users
+ frappe.set_user("test2@example.com")
+ self.assertEqual(update_password(new_password, old_password=old_password), "/")
+ # reset password
+ update_password(old_password, old_password=new_password)
+
+ # test API endpoint
+ with patch.object(user_module.frappe, 'sendmail') as sendmail:
+ frappe.clear_messages()
+ test_user = frappe.get_doc("User", "test2@example.com")
+ self.assertEqual(reset_password(user="test2@example.com"), None)
+ test_user.reload()
+ self.assertEqual(update_password(new_password, key=test_user.reset_password_key), "/")
+ update_password(old_password, old_password=new_password)
+ self.assertEqual(json.loads(frappe.message_log[0]), {"message": "Password reset instructions have been sent to your email"})
+ sendmail.assert_called_once()
+ self.assertEqual(sendmail.call_args[1]["recipients"], "test2@example.com")
+
+ self.assertEqual(reset_password(user="test2@example.com"), None)
+ self.assertEqual(reset_password(user="Administrator"), "not allowed")
+ self.assertEqual(reset_password(user="random"), "not found")
+
+ def test_user_onload_modules(self):
+ from frappe.config import get_modules_from_all_apps
+ from frappe.desk.form.load import getdoc
+ frappe.response.docs = []
+ getdoc("User", "Administrator")
+ doc = frappe.response.docs[0]
+ self.assertListEqual(doc.get("__onload").get('all_modules', []),
+ [m.get("module_name") for m in get_modules_from_all_apps()])
- # # Check that rollback works
- # frappe.db.rollback()
- # self.assertIsNone(frappe.db.get("User", {"email": email}))
def delete_contact(user):
- frappe.db.sql("DELETE FROM `tabContact` WHERE `email_id`= %s", user)
- frappe.db.sql("DELETE FROM `tabContact Email` WHERE `email_id`= %s", user)
+ frappe.db.delete("Contact", {"email_id": user})
+ frappe.db.delete("Contact Email", {"email_id": user})
diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js
index 819684cdfe..48dc2d1672 100644
--- a/frappe/core/doctype/user/user.js
+++ b/frappe/core/doctype/user/user.js
@@ -171,7 +171,7 @@ frappe.ui.form.on('User', {
frm.add_custom_button(__("Reset OTP Secret"), function() {
frappe.call({
- method: "frappe.core.doctype.user.user.reset_otp_secret",
+ method: "frappe.twofactor.reset_otp_secret",
args: {
"user": frm.doc.name
}
@@ -268,6 +268,7 @@ frappe.ui.form.on('User', {
callback: function(r) {
if (r.message) {
frappe.msgprint(__("Save API Secret: {0}", [r.message.api_secret]));
+ frm.reload_doc();
}
}
});
diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json
index 1d5f89897d..ea31e76a57 100644
--- a/frappe/core/doctype/user/user.json
+++ b/frappe/core/doctype/user/user.json
@@ -202,7 +202,8 @@
"fieldname": "role_profile_name",
"fieldtype": "Link",
"label": "Role Profile",
- "options": "Role Profile"
+ "options": "Role Profile",
+ "permlevel": 1
},
{
"fieldname": "roles_html",
@@ -554,20 +555,22 @@
"collapsible": 1,
"fieldname": "api_access",
"fieldtype": "Section Break",
- "label": "Api Access"
+ "label": "API Access"
},
{
- "description": "API Key cannot be regenerated",
+ "description": "API Key cannot be regenerated",
"fieldname": "api_key",
"fieldtype": "Data",
"label": "API Key",
+ "permlevel": 1,
"read_only": 1,
"unique": 1
},
{
"fieldname": "generate_keys",
"fieldtype": "Button",
- "label": "Generate Keys"
+ "label": "Generate Keys",
+ "permlevel": 1
},
{
"fieldname": "column_break_65",
@@ -577,6 +580,7 @@
"fieldname": "api_secret",
"fieldtype": "Password",
"label": "API Secret",
+ "permlevel": 1,
"read_only": 1
},
{
@@ -613,11 +617,6 @@
"link_doctype": "Contact",
"link_fieldname": "user"
},
- {
- "group": "Profile",
- "link_doctype": "Chat Profile",
- "link_fieldname": "user"
- },
{
"group": "Profile",
"link_doctype": "Blogger",
@@ -670,7 +669,7 @@
}
],
"max_attachments": 5,
- "modified": "2021-02-02 16:11:06.037543",
+ "modified": "2021-10-27 17:17:16.098457",
"modified_by": "Administrator",
"module": "Core",
"name": "User",
@@ -705,4 +704,4 @@
"sort_order": "DESC",
"title_field": "full_name",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index cf2b045c6d..fd19f4d82e 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
from bs4 import BeautifulSoup
import frappe
import frappe.share
@@ -13,19 +13,14 @@ from frappe.utils.password import update_password as _update_password, check_pas
from frappe.desk.notifications import clear_notifications
from frappe.desk.doctype.notification_settings.notification_settings import create_notification_settings, toggle_notifications
from frappe.utils.user import get_system_managers
-from frappe.website.utils import is_signup_enabled
+from frappe.website.utils import is_signup_disabled
from frappe.rate_limiter import rate_limit
-from frappe.utils.background_jobs import enqueue
from frappe.core.doctype.user_type.user_type import user_linked_with_permission_on_doctype
+from frappe.query_builder import DocType
STANDARD_USERS = ("Guest", "Administrator")
-
-class MaxUsersReachedError(frappe.ValidationError):
- pass
-
-
class User(Document):
__new_password = None
@@ -53,10 +48,9 @@ class User(Document):
def after_insert(self):
create_notification_settings(self.name)
frappe.cache().delete_key('users_for_mentions')
+ frappe.cache().delete_key('enabled_users')
def validate(self):
- self.check_demo()
-
# clear new password
self.__new_password = self.new_password
self.new_password = ""
@@ -130,14 +124,13 @@ class User(Document):
if self.has_value_changed('allow_in_mentions') or self.has_value_changed('user_type'):
frappe.cache().delete_key('users_for_mentions')
+ if self.has_value_changed('enabled'):
+ frappe.cache().delete_key('enabled_users')
+
def has_website_permission(self, ptype, user, verbose=False):
"""Returns true if current user is the session user"""
return self.name == frappe.session.user
- def check_demo(self):
- if frappe.session.user == 'demo@erpnext.com':
- frappe.throw(_('Cannot change user details in demo. Please signup for a new account at https://erpnext.com'), title=_('Not Allowed'))
-
def set_full_name(self):
self.full_name = " ".join(filter(None, [self.first_name, self.last_name]))
@@ -365,27 +358,31 @@ class User(Document):
frappe.local.login_manager.logout(user=self.name)
# delete todos
- frappe.db.sql("""DELETE FROM `tabToDo` WHERE `owner`=%s""", (self.name,))
+ frappe.db.delete("ToDo", {"owner": self.name})
frappe.db.sql("""UPDATE `tabToDo` SET `assigned_by`=NULL WHERE `assigned_by`=%s""",
(self.name,))
# delete events
- frappe.db.sql("""delete from `tabEvent` where owner=%s
- and event_type='Private'""", (self.name,))
+ frappe.db.delete("Event", {"owner": self.name, "event_type": "Private"})
# delete shares
- frappe.db.sql("""delete from `tabDocShare` where user=%s""", self.name)
-
+ frappe.db.delete("DocShare", {"user": self.name})
# delete messages
- frappe.db.sql("""delete from `tabCommunication`
- where communication_type in ('Chat', 'Notification')
- and reference_doctype='User'
- and (reference_name=%s or owner=%s)""", (self.name, self.name))
-
+ table = DocType("Communication")
+ frappe.db.delete(
+ table,
+ filters=(
+ (table.communication_type.isin(["Chat", "Notification"]))
+ & (table.reference_doctype == "User")
+ & ((table.reference_name == self.name) | table.owner == self.name)
+ ),
+ run=False,
+ )
# unlink contact
- frappe.db.sql("""update `tabContact`
- set `user`=null
- where `user`=%s""", (self.name))
+ table = DocType("Contact")
+ frappe.qb.update(table).where(
+ table.user == self.name
+ ).set(table.user, None).run()
# delete notification settings
frappe.delete_doc("Notification Settings", self.name, ignore_permissions=True)
@@ -393,9 +390,10 @@ class User(Document):
if self.get('allow_in_mentions'):
frappe.cache().delete_key('users_for_mentions')
+ frappe.cache().delete_key('enabled_users')
+
def before_rename(self, old_name, new_name, merge=False):
- self.check_demo()
frappe.clear_cache(user=old_name)
self.validate_rename(old_name, new_name)
@@ -424,16 +422,14 @@ class User(Document):
WHERE `%s` = %s""" %
(tab, field, '%s', field, '%s'), (new_name, old_name))
- if frappe.db.exists("Chat Profile", old_name):
- frappe.rename_doc("Chat Profile", old_name, new_name, force=True, show_alert=False)
-
if frappe.db.exists("Notification Settings", old_name):
frappe.rename_doc("Notification Settings", old_name, new_name, force=True, show_alert=False)
# set email
- frappe.db.sql("""UPDATE `tabUser`
- SET email = %s
- WHERE name = %s""", (new_name, new_name))
+ table = DocType("User")
+ frappe.qb.update(table).where(
+ table.name == new_name
+ ).set("email", new_name).run()
def append_roles(self, *roles):
"""Add roles to user"""
@@ -721,115 +717,33 @@ def get_email_awaiting(user):
where parent = %(user)s""",{"user":user})
return False
-@frappe.whitelist(allow_guest=False)
-def set_email_password(email_account, user, password):
- account = frappe.get_doc("Email Account", email_account)
- if account.awaiting_password:
- account.awaiting_password = 0
- account.password = password
- try:
- account.save(ignore_permissions=True)
- except Exception:
- frappe.db.rollback()
- return False
-
- return True
-
-def setup_user_email_inbox(email_account, awaiting_password, email_id, enable_outgoing):
- """ setup email inbox for user """
- def add_user_email(user):
- user = frappe.get_doc("User", user)
- row = user.append("user_emails", {})
-
- row.email_id = email_id
- row.email_account = email_account
- row.awaiting_password = awaiting_password or 0
- row.enable_outgoing = enable_outgoing or 0
-
- user.save(ignore_permissions=True)
-
- udpate_user_email_settings = False
- if not all([email_account, email_id]):
- return
-
- user_names = frappe.db.get_values("User", { "email": email_id }, as_dict=True)
- if not user_names:
- return
-
- for user in user_names:
- user_name = user.get("name")
-
- # check if inbox is alreay configured
- user_inbox = frappe.db.get_value("User Email", {
- "email_account": email_account,
- "parent": user_name
- }, ["name"]) or None
-
- if not user_inbox:
- add_user_email(user_name)
- else:
- # update awaiting password for email account
- udpate_user_email_settings = True
-
- if udpate_user_email_settings:
- frappe.db.sql("""UPDATE `tabUser Email` SET awaiting_password = %(awaiting_password)s,
- enable_outgoing = %(enable_outgoing)s WHERE email_account = %(email_account)s""", {
- "email_account": email_account,
- "enable_outgoing": enable_outgoing,
- "awaiting_password": awaiting_password or 0
- })
- else:
- users = " and ".join([frappe.bold(user.get("name")) for user in user_names])
- frappe.msgprint(_("Enabled email inbox for user {0}").format(users))
-
- ask_pass_update()
-
-def remove_user_email_inbox(email_account):
- """ remove user email inbox settings if email account is deleted """
- if not email_account:
- return
-
- users = frappe.get_all("User Email", filters={
- "email_account": email_account
- }, fields=["parent as name"])
-
- for user in users:
- doc = frappe.get_doc("User", user.get("name"))
- to_remove = [ row for row in doc.user_emails if row.email_account == email_account ]
- [ doc.remove(row) for row in to_remove ]
-
- doc.save(ignore_permissions=True)
-
def ask_pass_update():
# update the sys defaults as to awaiting users
from frappe.utils import set_default
- users = frappe.db.sql("""SELECT DISTINCT(parent) as user FROM `tabUser Email`
- WHERE awaiting_password = 1""", as_dict=True)
+ doctype = DocType("User Email")
+ users = frappe.qb.from_(doctype).where(doctype.awaiting_password == 1).select(
+ doctype.parent.as_("user")
+ ).distinct().run(as_dict=True)
password_list = [ user.get("user") for user in users ]
set_default("email_user_password", u','.join(password_list))
def _get_user_for_update_password(key, old_password):
# verify old password
+ result = frappe._dict()
if key:
- user = frappe.db.get_value("User", {"reset_password_key": key})
- if not user:
- return {
- 'message': _("The Link specified has either been used before or Invalid")
- }
+ result.user = frappe.db.get_value("User", {"reset_password_key": key})
+ if not result.user:
+ result.message = _("The Link specified has either been used before or Invalid")
elif old_password:
# verify old password
frappe.local.login_manager.check_password(frappe.session.user, old_password)
user = frappe.session.user
+ result.user = user
- else:
- return
-
- return {
- 'user': user
- }
+ return result
def reset_user_data(user):
user_doc = frappe.get_doc("User", user)
@@ -846,19 +760,17 @@ def verify_password(password):
@frappe.whitelist(allow_guest=True)
def sign_up(email, full_name, redirect_to):
- if not is_signup_enabled():
+ if is_signup_disabled():
frappe.throw(_('Sign Up is disabled'), title='Not Allowed')
user = frappe.db.get("User", {"email": email})
if user:
- if user.disabled:
- return 0, _("Registered but disabled")
- else:
+ if user.enabled:
return 0, _("Already Registered")
+ else:
+ return 0, _("Registered but disabled")
else:
- if frappe.db.sql("""select count(*) from tabUser where
- HOUR(TIMEDIFF(CURRENT_TIMESTAMP, TIMESTAMP(modified)))=1""")[0][0] > 300:
-
+ if frappe.db.get_creation_count('User', 60) > 300:
frappe.respond_as_web_page(_('Temporarily Disabled'),
_('Too many users signed up recently, so the registration is disabled. Please try back in an hour'),
http_status_code=429)
@@ -890,7 +802,7 @@ def sign_up(email, full_name, redirect_to):
return 2, _("Please ask your administrator to verify your sign-up")
@frappe.whitelist(allow_guest=True)
-@rate_limit(key='user', limit=get_password_reset_limit, seconds = 24*60*60, methods=['POST'])
+@rate_limit(limit=get_password_reset_limit, seconds = 24*60*60, methods=['POST'])
def reset_password(user):
if user=="Administrator":
return 'not allowed'
@@ -1051,91 +963,6 @@ def update_gravatar(name):
if gravatar:
frappe.db.set_value('User', name, 'user_image', gravatar)
-@frappe.whitelist(allow_guest=True)
-def send_token_via_sms(tmp_id,phone_no=None,user=None):
- try:
- from frappe.core.doctype.sms_settings.sms_settings import send_request
- except:
- return False
-
- if not frappe.cache().ttl(tmp_id + '_token'):
- return False
- ss = frappe.get_doc('SMS Settings', 'SMS Settings')
- if not ss.sms_gateway_url:
- return False
-
- token = frappe.cache().get(tmp_id + '_token')
- args = {ss.message_parameter: 'verification code is {}'.format(token)}
-
- for d in ss.get("parameters"):
- args[d.parameter] = d.value
-
- if user:
- user_phone = frappe.db.get_value('User', user, ['phone','mobile_no'], as_dict=1)
- usr_phone = user_phone.mobile_no or user_phone.phone
- if not usr_phone:
- return False
- else:
- if phone_no:
- usr_phone = phone_no
- else:
- return False
-
- args[ss.receiver_parameter] = usr_phone
- status = send_request(ss.sms_gateway_url, args, use_post=ss.use_post)
-
- if 200 <= status < 300:
- frappe.cache().delete(tmp_id + '_token')
- return True
- else:
- return False
-
-@frappe.whitelist(allow_guest=True)
-def send_token_via_email(tmp_id,token=None):
- import pyotp
-
- user = frappe.cache().get(tmp_id + '_user')
- count = token or frappe.cache().get(tmp_id + '_token')
-
- if ((not user) or (user == 'None') or (not count)):
- return False
- user_email = frappe.db.get_value('User',user, 'email')
- if not user_email:
- return False
-
- otpsecret = frappe.cache().get(tmp_id + '_otp_secret')
- hotp = pyotp.HOTP(otpsecret)
-
- frappe.sendmail(
- recipients=user_email,
- sender=None,
- subject="Verification Code",
- template="verification_code",
- args=dict(code=hotp.at(int(count))),
- delayed=False,
- retry=3
- )
-
- return True
-
-@frappe.whitelist(allow_guest=True)
-def reset_otp_secret(user):
- otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')
- user_email = frappe.db.get_value('User',user, 'email')
- if frappe.session.user in ["Administrator", user] :
- frappe.defaults.clear_default(user + '_otplogin')
- frappe.defaults.clear_default(user + '_otpsecret')
- email_args = {
- 'recipients':user_email, 'sender':None, 'subject':'OTP Secret Reset - {}'.format(otp_issuer or "Frappe Framework"),
- 'message':'Your OTP secret on {} has been reset. If you did not perform this reset and did not request it, please contact your System Administrator immediately.
'.format(otp_issuer or "Frappe Framework"),
- 'delayed':False,
- 'retry':3
- }
- enqueue(method=frappe.sendmail, queue='short', timeout=300, event=None, is_async=True, job_name=None, now=False, **email_args)
- return frappe.msgprint(_("OTP Secret has been reset. Re-registration will be required on next login."))
- else:
- return frappe.throw(_("OTP secret can only be reset by the Administrator."))
-
def throttle_user_creation():
if frappe.flags.in_import:
return
@@ -1153,15 +980,6 @@ def get_module_profile(module_profile):
module_profile = frappe.get_doc('Module Profile', {'module_profile_name': module_profile})
return module_profile.get('block_modules')
-def update_roles(role_profile):
- users = frappe.get_all('User', filters={'role_profile_name': role_profile})
- role_profile = frappe.get_doc('Role Profile', role_profile)
- roles = [role.role for role in role_profile.roles]
- for d in users:
- user = frappe.get_doc('User', d)
- user.set('roles', [])
- user.add_roles(*roles)
-
def create_contact(user, ignore_links=False, ignore_mandatory=False):
from frappe.contacts.doctype.contact.contact import get_contact_name
if user.name in ["Administrator", "Guest"]: return
@@ -1220,20 +1038,27 @@ def generate_keys(user):
:param user: str
"""
- if "System Manager" in frappe.get_roles():
- user_details = frappe.get_doc("User", user)
- api_secret = frappe.generate_hash(length=15)
- # if api key is not set generate api key
- if not user_details.api_key:
- api_key = frappe.generate_hash(length=15)
- user_details.api_key = api_key
- user_details.api_secret = api_secret
- user_details.save()
+ frappe.only_for("System Manager")
+ user_details = frappe.get_doc("User", user)
+ api_secret = frappe.generate_hash(length=15)
+ # if api key is not set generate api key
+ if not user_details.api_key:
+ api_key = frappe.generate_hash(length=15)
+ user_details.api_key = api_key
+ user_details.api_secret = api_secret
+ user_details.save()
+
+ return {"api_secret": api_secret}
- return {"api_secret": api_secret}
- frappe.throw(frappe._("Not Permitted"), frappe.PermissionError)
@frappe.whitelist()
def switch_theme(theme):
if theme in ["Dark", "Light"]:
frappe.db.set_value("User", frappe.session.user, "desk_theme", theme)
+
+def get_enabled_users():
+ def _get_enabled_users():
+ enabled_users = frappe.get_all("User", filters={"enabled": "1"}, pluck="name")
+ return enabled_users
+
+ return frappe.cache().get_value("enabled_users", _get_enabled_users)
diff --git a/frappe/core/doctype/user_document_type/user_document_type.py b/frappe/core/doctype/user_document_type/user_document_type.py
index 48dbf87b3d..a14d735e6a 100644
--- a/frappe/core/doctype/user_document_type/user_document_type.py
+++ b/frappe/core/doctype/user_document_type/user_document_type.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/user_email/user_email.py b/frappe/core/doctype/user_email/user_email.py
index 729aa03444..daad083577 100644
--- a/frappe/core/doctype/user_email/user_email.py
+++ b/frappe/core/doctype/user_email/user_email.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/user_group/test_user_group.py b/frappe/core/doctype/user_group/test_user_group.py
index 2f89d032e1..b5d642ae9c 100644
--- a/frappe/core/doctype/user_group/test_user_group.py
+++ b/frappe/core/doctype/user_group/test_user_group.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/core/doctype/user_group/user_group.py b/frappe/core/doctype/user_group/user_group.py
index 178775d407..05ff71e353 100644
--- a/frappe/core/doctype/user_group/user_group.py
+++ b/frappe/core/doctype/user_group/user_group.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/user_group_member/test_user_group_member.py b/frappe/core/doctype/user_group_member/test_user_group_member.py
index 8dbaed9e65..6d4650a3d0 100644
--- a/frappe/core/doctype/user_group_member/test_user_group_member.py
+++ b/frappe/core/doctype/user_group_member/test_user_group_member.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/core/doctype/user_group_member/user_group_member.py b/frappe/core/doctype/user_group_member/user_group_member.py
index f85ddc3209..69718d8d91 100644
--- a/frappe/core/doctype/user_group_member/user_group_member.py
+++ b/frappe/core/doctype/user_group_member/user_group_member.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/user_permission/test_user_permission.py b/frappe/core/doctype/user_permission/test_user_permission.py
index 1a442b53e7..cf905c2ce2 100644
--- a/frappe/core/doctype/user_permission/test_user_permission.py
+++ b/frappe/core/doctype/user_permission/test_user_permission.py
@@ -1,6 +1,5 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See LICENSE
from frappe.core.doctype.user_permission.user_permission import add_user_permissions, remove_applicable
from frappe.permissions import has_user_permission
from frappe.core.doctype.doctype.test_doctype import new_doctype
@@ -10,11 +9,14 @@ import unittest
class TestUserPermission(unittest.TestCase):
def setUp(self):
- frappe.db.sql("""DELETE FROM `tabUser Permission`
- WHERE `user` in (
- 'test_bulk_creation_update@example.com',
- 'test_user_perm1@example.com',
- 'nested_doc_user@example.com')""")
+ test_users = (
+ "test_bulk_creation_update@example.com",
+ "test_user_perm1@example.com",
+ "nested_doc_user@example.com",
+ )
+ frappe.db.delete("User Permission", {
+ "user": ("in", test_users)
+ })
frappe.delete_doc_if_exists("DocType", "Person")
frappe.db.sql_ddl("DROP TABLE IF EXISTS `tabPerson`")
frappe.delete_doc_if_exists("DocType", "Doc A")
@@ -71,7 +73,7 @@ class TestUserPermission(unittest.TestCase):
def test_for_applicable_on_update_from_apply_to_all(self):
''' Update User Permission from all to some applicable Doctypes'''
user = create_user('test_bulk_creation_update@example.com')
- param = get_params(user,'User', user.name, applicable = ["Chat Room", "Chat Message"])
+ param = get_params(user,'User', user.name, applicable = ["Comment", "Contact"])
# Initially create User Permission document with apply_to_all checked
is_created = add_user_permissions(get_params(user, 'User', user.name))
@@ -82,8 +84,8 @@ class TestUserPermission(unittest.TestCase):
frappe.db.commit()
removed_apply_to_all = frappe.db.exists("User Permission", get_exists_param(user))
- is_created_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Room"))
- is_created_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Message"))
+ is_created_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Comment"))
+ is_created_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Contact"))
# Check that apply_to_all is removed
self.assertIsNone(removed_apply_to_all)
@@ -99,14 +101,14 @@ class TestUserPermission(unittest.TestCase):
param = get_params(user, 'User', user.name)
# create User permissions that with applicable
- is_created = add_user_permissions(get_params(user, 'User', user.name, applicable = ["Chat Room", "Chat Message"]))
+ is_created = add_user_permissions(get_params(user, 'User', user.name, applicable = ["Comment", "Contact"]))
self.assertEqual(is_created, 1)
is_created = add_user_permissions(param)
is_created_apply_to_all = frappe.db.exists("User Permission", get_exists_param(user))
- removed_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Room"))
- removed_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Message"))
+ removed_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Comment"))
+ removed_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Contact"))
# To check that a User permission with apply_to_all exists
self.assertIsNotNone(is_created_apply_to_all)
diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py
index 4aa5797c7f..1366ace115 100644
--- a/frappe/core/doctype/user_permission/user_permission.py
+++ b/frappe/core/doctype/user_permission/user_permission.py
@@ -1,6 +1,5 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# Copyright (c) 2021, Frappe Technologies and contributors
+# License: MIT. See LICENSE
import frappe, json
from frappe.model.document import Document
@@ -55,7 +54,7 @@ class UserPermission(Document):
ref_link = frappe.get_desk_link(self.doctype, overlap_exists[0].name)
frappe.throw(_("{0} has already assigned default value for {1}.").format(ref_link, self.allow))
-@frappe.whitelist(allow_guest=True)
+@frappe.whitelist()
def get_user_permissions(user=None):
'''Get all users permissions for the user as a dict of doctype'''
# if this is called from client-side,
@@ -179,11 +178,16 @@ def check_applicable_doc_perm(user, doctype, docname):
@frappe.whitelist()
def clear_user_permissions(user, for_doctype):
- frappe.only_for('System Manager')
- total = frappe.db.count('User Permission', filters = dict(user=user, allow=for_doctype))
+ frappe.only_for("System Manager")
+ total = frappe.db.count("User Permission", {"user": user, "allow": for_doctype})
+
if total:
- frappe.db.sql('DELETE FROM `tabUser Permission` WHERE `user`=%s AND `allow`=%s', (user, for_doctype))
+ frappe.db.delete("User Permission", {
+ "allow": for_doctype,
+ "user": user,
+ })
frappe.clear_cache()
+
return total
@frappe.whitelist()
@@ -225,7 +229,7 @@ def insert_user_perm(user, doctype, docname, is_default=0, hide_descendants=0, a
user_perm.is_default = is_default
user_perm.hide_descendants = hide_descendants
if applicable:
- user_perm.applicable_for = applicable
+ user_perm.applicable_for = applicable
user_perm.apply_to_all_doctypes = 0
else:
user_perm.apply_to_all_doctypes = 1
@@ -233,27 +237,27 @@ def insert_user_perm(user, doctype, docname, is_default=0, hide_descendants=0, a
def remove_applicable(perm_applied_docs, user, doctype, docname):
for applicable_for in perm_applied_docs:
- frappe.db.sql("""DELETE FROM `tabUser Permission`
- WHERE `user`=%s
- AND `applicable_for`=%s
- AND `allow`=%s
- AND `for_value`=%s
- """, (user, applicable_for, doctype, docname))
+ frappe.db.delete("User Permission", {
+ "applicable_for": applicable_for,
+ "for_value": docname,
+ "allow": doctype,
+ "user": user,
+ })
def remove_apply_to_all(user, doctype, docname):
- frappe.db.sql("""DELETE from `tabUser Permission`
- WHERE `user`=%s
- AND `apply_to_all_doctypes`=1
- AND `allow`=%s
- AND `for_value`=%s
- """,(user, doctype, docname))
+ frappe.db.delete("User Permission", {
+ "apply_to_all_doctypes": 1,
+ "for_value": docname,
+ "allow": doctype,
+ "user": user,
+ })
def update_applicable(already_applied, to_apply, user, doctype, docname):
for applied in already_applied:
if applied not in to_apply:
- frappe.db.sql("""DELETE FROM `tabUser Permission`
- WHERE `user`=%s
- AND `applicable_for`=%s
- AND `allow`=%s
- AND `for_value`=%s
- """,(user, applied, doctype, docname))
+ frappe.db.delete("User Permission", {
+ "applicable_for": applied,
+ "for_value": docname,
+ "allow": doctype,
+ "user": user,
+ })
diff --git a/frappe/core/doctype/user_select_document_type/user_select_document_type.py b/frappe/core/doctype/user_select_document_type/user_select_document_type.py
index 13e3f0d351..18a21931e5 100644
--- a/frappe/core/doctype/user_select_document_type/user_select_document_type.py
+++ b/frappe/core/doctype/user_select_document_type/user_select_document_type.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/user_social_login/user_social_login.py b/frappe/core/doctype/user_social_login/user_social_login.py
index 4a34006d2b..80c0c89383 100644
--- a/frappe/core/doctype/user_social_login/user_social_login.py
+++ b/frappe/core/doctype/user_social_login/user_social_login.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from frappe.model.document import Document
diff --git a/frappe/core/doctype/user_type/test_user_type.py b/frappe/core/doctype/user_type/test_user_type.py
index 1c47f02bbb..7080e1830b 100644
--- a/frappe/core/doctype/user_type/test_user_type.py
+++ b/frappe/core/doctype/user_type/test_user_type.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/core/doctype/user_type/user_type.py b/frappe/core/doctype/user_type/user_type.py
index 82ffb090f1..c1fd678141 100644
--- a/frappe/core/doctype/user_type/user_type.py
+++ b/frappe/core/doctype/user_type/user_type.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
@@ -36,8 +36,11 @@ class UserType(Document):
if not self.user_doctypes:
return
- modules = frappe.get_all('DocType', fields=['distinct module as module'],
- filters={'name': ('in', [d.document_type for d in self.user_doctypes])})
+ modules = frappe.get_all("DocType",
+ fields=["module"],
+ filters={"name": ("in", [d.document_type for d in self.user_doctypes])},
+ distinct=True,
+ )
self.set('user_type_modules', [])
for row in modules:
@@ -192,7 +195,7 @@ def get_user_linked_doctypes(doctype, txt, searchfield, start, page_len, filters
['DocType', 'read_only', '=', 0], ['DocType', 'name', 'like', '%{0}%'.format(txt)]]
doctypes = frappe.get_all('DocType', fields = ['`tabDocType`.`name`'], filters=filters,
- order_by = '`tabDocType`.`idx` desc', limit_start=start, limit_page_length=page_len, as_list=1, debug=1)
+ order_by = '`tabDocType`.`idx` desc', limit_start=start, limit_page_length=page_len, as_list=1)
custom_dt_filters = [['Custom Field', 'dt', 'like', '%{0}%'.format(txt)],
['Custom Field', 'options', '=', 'User'], ['Custom Field', 'fieldtype', '=', 'Link']]
diff --git a/frappe/core/doctype/user_type_module/user_type_module.py b/frappe/core/doctype/user_type_module/user_type_module.py
index 9afbcd294d..d25479f869 100644
--- a/frappe/core/doctype/user_type_module/user_type_module.py
+++ b/frappe/core/doctype/user_type_module/user_type_module.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/version/test_version.py b/frappe/core/doctype/version/test_version.py
index f6c099c4ea..608dc9f0ab 100644
--- a/frappe/core/doctype/version/test_version.py
+++ b/frappe/core/doctype/version/test_version.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest, copy
from frappe.test_runner import make_test_objects
diff --git a/frappe/core/doctype/version/version.css b/frappe/core/doctype/version/version.css
deleted file mode 100644
index 769b352585..0000000000
--- a/frappe/core/doctype/version/version.css
+++ /dev/null
@@ -1,21 +0,0 @@
-.version-info {
- overflow: auto;
-}
-
-.version-info pre {
- border: 0px;
- margin: 0px;
- background-color: inherit;
-}
-
-.version-info .table {
- background-color: inherit;
-}
-
-.version-info .success {
- background-color: #dff0d8 !important;
-}
-
-.version-info .danger {
- background-color: #f2dede !important;
-}
diff --git a/frappe/core/doctype/version/version.py b/frappe/core/doctype/version/version.py
index a1bd851346..fcb558650a 100644
--- a/frappe/core/doctype/version/version.py
+++ b/frappe/core/doctype/version/version.py
@@ -1,7 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe, json
diff --git a/frappe/core/doctype/view_log/test_view_log.py b/frappe/core/doctype/view_log/test_view_log.py
index 025f3d8ad9..efa9538fbf 100644
--- a/frappe/core/doctype/view_log/test_view_log.py
+++ b/frappe/core/doctype/view_log/test_view_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/core/doctype/view_log/view_log.json b/frappe/core/doctype/view_log/view_log.json
index 6c3247c58f..3c4486c944 100644
--- a/frappe/core/doctype/view_log/view_log.json
+++ b/frappe/core/doctype/view_log/view_log.json
@@ -125,7 +125,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2019-09-05 14:22:27.664645",
+ "modified": "2021-10-25 14:22:27.664645",
"modified_by": "Administrator",
"module": "Core",
"name": "View Log",
@@ -158,7 +158,6 @@
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
- "track_changes": 1,
"track_seen": 0,
"track_views": 0
-}
\ No newline at end of file
+}
diff --git a/frappe/core/doctype/view_log/view_log.py b/frappe/core/doctype/view_log/view_log.py
index 242250be8b..fbbd6e1154 100644
--- a/frappe/core/doctype/view_log/view_log.py
+++ b/frappe/core/doctype/view_log/view_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/core/notifications.py b/frappe/core/notifications.py
index 707de43f28..b43d424df5 100644
--- a/frappe/core/notifications.py
+++ b/frappe/core/notifications.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/core/page/__init__.py b/frappe/core/page/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/core/page/__init__.py
+++ b/frappe/core/page/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py
index 847b23bd3e..4d9deca526 100644
--- a/frappe/core/page/background_jobs/background_jobs.py
+++ b/frappe/core/page/background_jobs/background_jobs.py
@@ -1,15 +1,15 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import json
from typing import TYPE_CHECKING, Dict, List
-from rq import Queue, Worker
+from rq import Worker
import frappe
from frappe import _
from frappe.utils import convert_utc_to_user_timezone, format_datetime
-from frappe.utils.background_jobs import get_redis_conn
+from frappe.utils.background_jobs import get_redis_conn, get_queues
from frappe.utils.scheduler import is_scheduler_inactive
if TYPE_CHECKING:
@@ -29,7 +29,7 @@ def get_info(show_failed=False) -> List[Dict]:
show_failed = json.loads(show_failed)
conn = get_redis_conn()
- queues = Queue.all(conn)
+ queues = get_queues()
workers = Worker.all(conn)
jobs = []
@@ -67,7 +67,8 @@ def get_info(show_failed=False) -> List[Dict]:
fail_registry = queue.failed_job_registry
for job_id in fail_registry.get_job_ids():
job = queue.fetch_job(job_id)
- add_job(job, queue.name)
+ if job:
+ add_job(job, queue.name)
return jobs
@@ -75,7 +76,7 @@ def get_info(show_failed=False) -> List[Dict]:
@frappe.whitelist()
def remove_failed_jobs():
conn = get_redis_conn()
- queues = Queue.all(conn)
+ queues = get_queues()
for queue in queues:
fail_registry = queue.failed_job_registry
for job_id in fail_registry.get_job_ids():
diff --git a/frappe/core/page/permission_manager/__init__.py b/frappe/core/page/permission_manager/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/core/page/permission_manager/__init__.py
+++ b/frappe/core/page/permission_manager/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js
index 41cc900a97..6b427fdebf 100644
--- a/frappe/core/page/permission_manager/permission_manager.js
+++ b/frappe/core/page/permission_manager/permission_manager.js
@@ -325,15 +325,15 @@ frappe.PermissionEngine = class PermissionEngine {
.attr("data-doctype", d.parent)
.attr("data-role", d.role)
.attr("data-permlevel", d.permlevel)
- .click(function () {
+ .on("click", () => {
return frappe.call({
module: "frappe.core",
page: "permission_manager",
method: "remove",
args: {
- doctype: $(this).attr("data-doctype"),
- role: $(this).attr("data-role"),
- permlevel: $(this).attr("data-permlevel")
+ doctype: d.parent,
+ role: d.role,
+ permlevel: d.permlevel
},
callback: (r) => {
if (r.exc) {
diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py
index 15c7cb55ae..08642c599e 100644
--- a/frappe/core/page/permission_manager/permission_manager.py
+++ b/frappe/core/page/permission_manager/permission_manager.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
@@ -92,14 +92,14 @@ def update(doctype, role, permlevel, ptype, value=None):
"""Update role permission params
Args:
- doctype (str): Name of the DocType to update params for
- role (str): Role to be updated for, eg "Website Manager".
- permlevel (int): perm level the provided rule applies to
- ptype (str): permission type, example "read", "delete", etc.
- value (None, optional): value for ptype, None indicates False
+ doctype (str): Name of the DocType to update params for
+ role (str): Role to be updated for, eg "Website Manager".
+ permlevel (int): perm level the provided rule applies to
+ ptype (str): permission type, example "read", "delete", etc.
+ value (None, optional): value for ptype, None indicates False
Returns:
- str: Refresh flag is permission is updated successfully
+ str: Refresh flag is permission is updated successfully
"""
frappe.only_for("System Manager")
out = update_permission_property(doctype, role, permlevel, ptype, value)
@@ -110,10 +110,9 @@ def remove(doctype, role, permlevel):
frappe.only_for("System Manager")
setup_custom_perms(doctype)
- name = frappe.get_value('Custom DocPerm', dict(parent=doctype, role=role, permlevel=permlevel))
+ frappe.db.delete("Custom DocPerm", {"parent": doctype, "role": role, "permlevel": permlevel})
- frappe.db.sql('delete from `tabCustom DocPerm` where name=%s', name)
- if not frappe.get_all('Custom DocPerm', dict(parent=doctype)):
+ if not frappe.get_all('Custom DocPerm', {"parent": doctype}):
frappe.throw(_('There must be atleast one permission rule.'), title=_('Cannot Remove'))
validate_permissions_for_doctype(doctype, for_remove=True, alert=True)
diff --git a/frappe/core/report/__init__.py b/frappe/core/report/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/core/report/__init__.py
+++ b/frappe/core/report/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py
index 13602ca777..535d354250 100644
--- a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py
+++ b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _, throw
diff --git a/frappe/core/report/transaction_log_report/transaction_log_report.py b/frappe/core/report/transaction_log_report/transaction_log_report.py
index ff8d8345d6..e9c68cb0c7 100644
--- a/frappe/core/report/transaction_log_report/transaction_log_report.py
+++ b/frappe/core/report/transaction_log_report/transaction_log_report.py
@@ -1,5 +1,5 @@
-# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# Copyright (c) 2021, Frappe Technologies and contributors
+# License: MIT. See LICENSE
import frappe
import hashlib
@@ -12,13 +12,17 @@ def execute(filters=None):
return columns, data
def get_data(filters=None):
-
- logs = frappe.db.sql("SELECT * FROM `tabTransaction Log` order by creation desc ", as_dict=1)
result = []
+ logs = frappe.get_all("Transaction Log", fields=["*"], order_by="creation desc")
+
for l in logs:
row_index = int(l.row_index)
if row_index > 1:
- previous_hash = frappe.db.sql("SELECT chaining_hash FROM `tabTransaction Log` WHERE row_index = {0}".format(row_index - 1))
+ previous_hash = frappe.get_all(
+ "Transaction Log",
+ fields=["chaining_hash"],
+ filters={"row_index": row_index - 1},
+ )
if not previous_hash:
integrity = False
else:
diff --git a/frappe/core/utils.py b/frappe/core/utils.py
index 9b8ee3a326..d4690cae89 100644
--- a/frappe/core/utils.py
+++ b/frappe/core/utils.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/core/workspace/build/build.json b/frappe/core/workspace/build/build.json
index aefda698b1..aabb4f9d1c 100644
--- a/frappe/core/workspace/build/build.json
+++ b/frappe/core/workspace/build/build.json
@@ -1,24 +1,20 @@
{
- "cards_label": "Elements",
- "category": "Modules",
"charts": [],
+ "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"DocType\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Workspace\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Report\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Elements\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Modules\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Models\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Views\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Scripting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Packages\",\"col\":4}}]",
"creation": "2021-01-02 10:51:16.579957",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends_another_page": 0,
+ "for_user": "",
"hide_custom": 0,
"icon": "tool",
"idx": 0,
- "is_default": 0,
- "is_standard": 1,
"label": "Build",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Modules",
+ "link_count": 0,
"link_type": "DocType",
"onboard": 0,
"only_for": "",
@@ -28,6 +24,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Module Def",
+ "link_count": 0,
"link_to": "Module Def",
"link_type": "DocType",
"onboard": 0,
@@ -38,6 +35,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Workspace",
+ "link_count": 0,
"link_to": "Workspace",
"link_type": "DocType",
"onboard": 0,
@@ -48,6 +46,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Module Onboarding",
+ "link_count": 0,
"link_to": "Module Onboarding",
"link_type": "DocType",
"onboard": 0,
@@ -58,6 +57,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Block Module",
+ "link_count": 0,
"link_to": "Block Module",
"link_type": "DocType",
"onboard": 0,
@@ -68,6 +68,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Models",
+ "link_count": 0,
"link_type": "DocType",
"onboard": 0,
"only_for": "",
@@ -77,6 +78,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "DocType",
+ "link_count": 0,
"link_to": "DocType",
"link_type": "DocType",
"onboard": 0,
@@ -87,6 +89,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Workflow",
+ "link_count": 0,
"link_to": "Workflow",
"link_type": "DocType",
"onboard": 0,
@@ -97,6 +100,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Views",
+ "link_count": 0,
"link_type": "DocType",
"onboard": 0,
"only_for": "",
@@ -106,6 +110,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Report",
+ "link_count": 0,
"link_to": "Report",
"link_type": "DocType",
"onboard": 0,
@@ -116,6 +121,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Print Format",
+ "link_count": 0,
"link_to": "Print Format",
"link_type": "DocType",
"onboard": 0,
@@ -126,6 +132,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Workspace",
+ "link_count": 0,
"link_to": "Workspace",
"link_type": "DocType",
"onboard": 0,
@@ -136,6 +143,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Dashboard",
+ "link_count": 0,
"link_to": "Dashboard",
"link_type": "DocType",
"onboard": 0,
@@ -146,6 +154,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Scripting",
+ "link_count": 0,
"link_type": "DocType",
"onboard": 0,
"only_for": "",
@@ -155,6 +164,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Server Script",
+ "link_count": 0,
"link_to": "Server Script",
"link_type": "DocType",
"onboard": 0,
@@ -165,6 +175,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Client Script",
+ "link_count": 0,
"link_to": "Client Script",
"link_type": "DocType",
"onboard": 0,
@@ -175,20 +186,52 @@
"hidden": 0,
"is_query_report": 0,
"label": "Scheduled Job Type",
+ "link_count": 0,
"link_to": "Scheduled Job Type",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Packages",
+ "link_count": 2,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Package",
+ "link_count": 0,
+ "link_to": "Package",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Package Import",
+ "link_count": 0,
+ "link_to": "Package Import",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
}
],
- "modified": "2021-02-04 13:48:48.493146",
+ "modified": "2021-09-05 21:14:52.384816",
"modified_by": "Administrator",
"module": "Core",
"name": "Build",
"owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
+ "parent_page": "",
+ "public": 1,
+ "restrict_to_domain": "",
+ "roles": [],
+ "sequence_id": 5,
"shortcuts": [
{
"doc_view": "",
@@ -208,5 +251,6 @@
"link_to": "Report",
"type": "DocType"
}
- ]
+ ],
+ "title": "Build"
}
\ No newline at end of file
diff --git a/frappe/core/workspace/settings/settings.json b/frappe/core/workspace/settings/settings.json
index fb26b73cfc..917ce2cbdc 100644
--- a/frappe/core/workspace/settings/settings.json
+++ b/frappe/core/workspace/settings/settings.json
@@ -1,22 +1,20 @@
{
- "category": "Modules",
"charts": [],
+ "content": "[{\"type\":\"header\",\"data\": {\"text\":\"Settings\",\"level\": 4,\"col\": 12}}, {\"type\":\"shortcut\",\"data\": {\"shortcut_name\":\"System Settings\",\"col\": 4}}, {\"type\":\"shortcut\",\"data\": {\"shortcut_name\":\"Print Settings\",\"col\": 4}}, {\"type\":\"shortcut\",\"data\": {\"shortcut_name\":\"Website Settings\",\"col\": 4}}, {\"type\":\"spacer\",\"data\": {\"col\": 12}}, {\"type\":\"header\",\"data\": {\"text\":\"Reports & Masters\",\"level\": 4,\"col\": 12}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Data\",\"col\": 4}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Email / Notifications\",\"col\": 4}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Website\",\"col\": 4}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Core\",\"col\": 4}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Printing\",\"col\": 4}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Workflow\",\"col\": 4}}]",
"creation": "2020-03-02 15:09:40.527211",
- "developer_mode_only": 0,
- "disable_user_customization": 1,
"docstatus": 0,
"doctype": "Workspace",
- "extends_another_page": 0,
+ "for_user": "",
"hide_custom": 0,
"icon": "setting",
"idx": 0,
- "is_standard": 1,
"label": "Settings",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Data",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -25,6 +23,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Import Data",
+ "link_count": 0,
"link_to": "Data Import",
"link_type": "DocType",
"onboard": 0,
@@ -35,6 +34,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Export Data",
+ "link_count": 0,
"link_to": "Data Export",
"link_type": "DocType",
"onboard": 0,
@@ -45,6 +45,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Bulk Update",
+ "link_count": 0,
"link_to": "Bulk Update",
"link_type": "DocType",
"onboard": 0,
@@ -55,6 +56,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Download Backups",
+ "link_count": 0,
"link_to": "backups",
"link_type": "Page",
"onboard": 0,
@@ -65,6 +67,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Deleted Documents",
+ "link_count": 0,
"link_to": "Deleted Document",
"link_type": "DocType",
"onboard": 0,
@@ -74,6 +77,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Email / Notifications",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -82,6 +86,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Email Account",
+ "link_count": 0,
"link_to": "Email Account",
"link_type": "DocType",
"onboard": 0,
@@ -92,6 +97,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Email Domain",
+ "link_count": 0,
"link_to": "Email Domain",
"link_type": "DocType",
"onboard": 0,
@@ -102,6 +108,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Notification",
+ "link_count": 0,
"link_to": "Notification",
"link_type": "DocType",
"onboard": 0,
@@ -112,6 +119,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Email Template",
+ "link_count": 0,
"link_to": "Email Template",
"link_type": "DocType",
"onboard": 0,
@@ -122,6 +130,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Auto Email Report",
+ "link_count": 0,
"link_to": "Auto Email Report",
"link_type": "DocType",
"onboard": 0,
@@ -132,6 +141,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Newsletter",
+ "link_count": 0,
"link_to": "Newsletter",
"link_type": "DocType",
"onboard": 0,
@@ -142,6 +152,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Notification Settings",
+ "link_count": 0,
"link_to": "Notification Settings",
"link_type": "DocType",
"onboard": 0,
@@ -151,6 +162,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Website",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -159,6 +171,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Website Settings",
+ "link_count": 0,
"link_to": "Website Settings",
"link_type": "DocType",
"onboard": 1,
@@ -169,6 +182,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Website Theme",
+ "link_count": 0,
"link_to": "Website Theme",
"link_type": "DocType",
"onboard": 1,
@@ -179,6 +193,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Website Script",
+ "link_count": 0,
"link_to": "Website Script",
"link_type": "DocType",
"onboard": 0,
@@ -189,6 +204,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "About Us Settings",
+ "link_count": 0,
"link_to": "About Us Settings",
"link_type": "DocType",
"onboard": 0,
@@ -199,6 +215,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Contact Us Settings",
+ "link_count": 0,
"link_to": "Contact Us Settings",
"link_type": "DocType",
"onboard": 0,
@@ -208,6 +225,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Core",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -216,6 +234,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "System Settings",
+ "link_count": 0,
"link_to": "System Settings",
"link_type": "DocType",
"onboard": 0,
@@ -226,6 +245,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Error Log",
+ "link_count": 0,
"link_to": "Error Log",
"link_type": "DocType",
"onboard": 0,
@@ -236,6 +256,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Error Snapshot",
+ "link_count": 0,
"link_to": "Error Snapshot",
"link_type": "DocType",
"onboard": 0,
@@ -246,6 +267,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Domain Settings",
+ "link_count": 0,
"link_to": "Domain Settings",
"link_type": "DocType",
"onboard": 0,
@@ -255,6 +277,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Printing",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -263,6 +286,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Print Format Builder",
+ "link_count": 0,
"link_to": "print-format-builder",
"link_type": "Page",
"onboard": 0,
@@ -273,6 +297,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Print Settings",
+ "link_count": 0,
"link_to": "Print Settings",
"link_type": "DocType",
"onboard": 0,
@@ -283,6 +308,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Print Format",
+ "link_count": 0,
"link_to": "Print Format",
"link_type": "DocType",
"onboard": 0,
@@ -293,6 +319,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Print Style",
+ "link_count": 0,
"link_to": "Print Style",
"link_type": "DocType",
"onboard": 0,
@@ -302,6 +329,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Workflow",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -310,6 +338,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Workflow",
+ "link_count": 0,
"link_to": "Workflow",
"link_type": "DocType",
"onboard": 0,
@@ -320,6 +349,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Workflow State",
+ "link_count": 0,
"link_to": "Workflow State",
"link_type": "DocType",
"onboard": 0,
@@ -330,19 +360,23 @@
"hidden": 0,
"is_query_report": 0,
"label": "Workflow Action",
+ "link_count": 0,
"link_to": "Workflow Action",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
- "modified": "2020-12-01 13:38:40.235323",
+ "modified": "2021-08-05 12:16:03.456174",
"modified_by": "Administrator",
"module": "Core",
"name": "Settings",
"owner": "Administrator",
- "pin_to_bottom": 1,
- "pin_to_top": 0,
+ "parent_page": "",
+ "public": 1,
+ "restrict_to_domain": "",
+ "roles": [],
+ "sequence_id": 29,
"shortcuts": [
{
"icon": "setting",
@@ -363,5 +397,5 @@
"type": "DocType"
}
],
- "shortcuts_label": "Settings"
+ "title": "Settings"
}
\ No newline at end of file
diff --git a/frappe/core/workspace/users/users.json b/frappe/core/workspace/users/users.json
index ba82461b57..85c110151b 100644
--- a/frappe/core/workspace/users/users.json
+++ b/frappe/core/workspace/users/users.json
@@ -1,23 +1,20 @@
{
- "category": "Administration",
"charts": [],
+ "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"User\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Role\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Permission Manager\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"User Profile\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"User Type\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Users\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Logs\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Permissions\", \"col\": 4}}]",
"creation": "2020-03-02 15:12:16.754449",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends_another_page": 0,
+ "for_user": "",
"hide_custom": 0,
"icon": "users",
"idx": 0,
- "is_default": 0,
- "is_standard": 1,
"label": "Users",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Users",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -26,6 +23,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "User",
+ "link_count": 0,
"link_to": "User",
"link_type": "DocType",
"onboard": 0,
@@ -36,6 +34,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Role",
+ "link_count": 0,
"link_to": "Role",
"link_type": "DocType",
"onboard": 0,
@@ -46,6 +45,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Role Profile",
+ "link_count": 0,
"link_to": "Role Profile",
"link_type": "DocType",
"onboard": 0,
@@ -55,6 +55,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Logs",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -63,6 +64,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Activity Log",
+ "link_count": 0,
"link_to": "Activity Log",
"link_type": "DocType",
"onboard": 0,
@@ -73,6 +75,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Access Log",
+ "link_count": 0,
"link_to": "Access Log",
"link_type": "DocType",
"onboard": 0,
@@ -82,6 +85,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Permissions",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -90,6 +94,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Role Permissions Manager",
+ "link_count": 0,
"link_to": "permission-manager",
"link_type": "Page",
"onboard": 0,
@@ -100,6 +105,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "User Permissions",
+ "link_count": 0,
"link_to": "User Permission",
"link_type": "DocType",
"onboard": 0,
@@ -110,6 +116,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Role Permission for Page and Report",
+ "link_count": 0,
"link_to": "Role Permission for Page and Report",
"link_type": "DocType",
"onboard": 0,
@@ -120,6 +127,7 @@
"hidden": 0,
"is_query_report": 1,
"label": "Permitted Documents For User",
+ "link_count": 0,
"link_to": "Permitted Documents For User",
"link_type": "Report",
"onboard": 0,
@@ -130,19 +138,23 @@
"hidden": 0,
"is_query_report": 0,
"label": "Document Share Report",
+ "link_count": 0,
"link_to": "Document Share Report",
"link_type": "Report",
"onboard": 0,
"type": "Link"
}
],
- "modified": "2021-03-25 23:02:34.582569",
+ "modified": "2021-08-05 12:16:03.010205",
"modified_by": "Administrator",
"module": "Core",
"name": "Users",
"owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
+ "parent_page": "",
+ "public": 1,
+ "restrict_to_domain": "",
+ "roles": [],
+ "sequence_id": 27,
"shortcuts": [
{
"label": "User",
@@ -170,5 +182,6 @@
"link_to": "User Type",
"type": "DocType"
}
- ]
+ ],
+ "title": "Users"
}
\ No newline at end of file
diff --git a/frappe/coverage.py b/frappe/coverage.py
new file mode 100644
index 0000000000..1969cae141
--- /dev/null
+++ b/frappe/coverage.py
@@ -0,0 +1,62 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See LICENSE
+"""
+ frappe.coverage
+ ~~~~~~~~~~~~~~~~
+
+ Coverage settings for frappe
+"""
+
+STANDARD_INCLUSIONS = ["*.py"]
+
+STANDARD_EXCLUSIONS = [
+ '*.js',
+ '*.xml',
+ '*.pyc',
+ '*.css',
+ '*.less',
+ '*.scss',
+ '*.vue',
+ '*.html',
+ '*/test_*',
+ '*/node_modules/*',
+ '*/doctype/*/*_dashboard.py',
+ '*/patches/*',
+]
+
+FRAPPE_EXCLUSIONS = [
+ "*/tests/*",
+ "*/commands/*",
+ "*/frappe/change_log/*",
+ "*/frappe/exceptions*",
+ "*frappe/setup.py",
+ "*/doctype/*/*_dashboard.py",
+ "*/patches/*",
+]
+
+class CodeCoverage():
+ def __init__(self, with_coverage, app):
+ self.with_coverage = with_coverage
+ self.app = app or 'frappe'
+
+ def __enter__(self):
+ if self.with_coverage:
+ import os
+ from coverage import Coverage
+ from frappe.utils import get_bench_path
+
+ # Generate coverage report only for app that is being tested
+ source_path = os.path.join(get_bench_path(), 'apps', self.app)
+ omit = STANDARD_EXCLUSIONS[:]
+
+ if self.app == 'frappe':
+ omit.extend(FRAPPE_EXCLUSIONS)
+
+ self.coverage = Coverage(source=[source_path], omit=omit, include=STANDARD_INCLUSIONS)
+ self.coverage.start()
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ if self.with_coverage:
+ self.coverage.stop()
+ self.coverage.save()
+ self.coverage.xml_report()
\ No newline at end of file
diff --git a/frappe/custom/doctype/client_script/__init__.py b/frappe/custom/doctype/client_script/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/custom/doctype/client_script/__init__.py
+++ b/frappe/custom/doctype/client_script/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/custom/doctype/client_script/client_script.json b/frappe/custom/doctype/client_script/client_script.json
index db02d8d4bc..50f6bf3cc4 100644
--- a/frappe/custom/doctype/client_script/client_script.json
+++ b/frappe/custom/doctype/client_script/client_script.json
@@ -9,7 +9,10 @@
"field_order": [
"dt",
"view",
+ "column_break_3",
+ "module",
"enabled",
+ "section_break_6",
"script",
"sample"
],
@@ -53,13 +56,27 @@
"label": "Apply To",
"options": "List\nForm",
"set_only_once": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module (for export)",
+ "options": "Module Def"
+ },
+ {
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
}
],
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-03-16 20:33:51.400191",
+ "modified": "2021-09-04 12:03:27.029815",
"modified_by": "Administrator",
"module": "Custom",
"name": "Client Script",
diff --git a/frappe/custom/doctype/client_script/client_script.py b/frappe/custom/doctype/client_script/client_script.py
index 9c098fe8c9..fd6bc9accd 100644
--- a/frappe/custom/doctype/client_script/client_script.py
+++ b/frappe/custom/doctype/client_script/client_script.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
diff --git a/frappe/custom/doctype/client_script/test_client_script.py b/frappe/custom/doctype/client_script/test_client_script.py
index b8358468b9..4887956001 100644
--- a/frappe/custom/doctype/client_script/test_client_script.py
+++ b/frappe/custom/doctype/client_script/test_client_script.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/custom/doctype/custom_field/__init__.py b/frappe/custom/doctype/custom_field/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/custom/doctype/custom_field/__init__.py
+++ b/frappe/custom/doctype/custom_field/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json
index 2f0819ab68..235f11aad8 100644
--- a/frappe/custom/doctype/custom_field/custom_field.json
+++ b/frappe/custom/doctype/custom_field/custom_field.json
@@ -1,453 +1,458 @@
{
- "actions": [],
- "allow_import": 1,
- "creation": "2013-01-10 16:34:01",
- "description": "Adds a custom field to a DocType",
- "doctype": "DocType",
- "document_type": "Setup",
- "engine": "InnoDB",
- "field_order": [
- "dt",
- "label",
- "label_help",
- "fieldname",
- "insert_after",
- "length",
- "column_break_6",
- "fieldtype",
- "precision",
- "hide_seconds",
- "hide_days",
- "options",
- "fetch_from",
- "fetch_if_empty",
- "options_help",
- "section_break_11",
- "collapsible",
- "collapsible_depends_on",
- "default",
- "depends_on",
- "mandatory_depends_on",
- "read_only_depends_on",
- "properties",
- "non_negative",
- "reqd",
- "unique",
- "read_only",
- "ignore_user_permissions",
- "hidden",
- "print_hide",
- "print_hide_if_no_value",
- "print_width",
- "no_copy",
- "allow_on_submit",
- "in_list_view",
- "in_standard_filter",
- "in_global_search",
- "in_preview",
- "bold",
- "report_hide",
- "search_index",
- "allow_in_quick_entry",
- "ignore_xss_filter",
- "translatable",
- "hide_border",
- "description",
- "permlevel",
- "width",
- "columns"
- ],
- "fields": [
- {
- "bold": 1,
- "fieldname": "dt",
- "fieldtype": "Link",
- "in_filter": 1,
- "in_list_view": 1,
- "label": "Document",
- "oldfieldname": "dt",
- "oldfieldtype": "Link",
- "options": "DocType",
- "reqd": 1,
- "search_index": 1
- },
- {
- "bold": 1,
- "fieldname": "label",
- "fieldtype": "Data",
- "in_filter": 1,
- "label": "Label",
- "no_copy": 1,
- "oldfieldname": "label",
- "oldfieldtype": "Data"
- },
- {
- "fieldname": "label_help",
- "fieldtype": "HTML",
- "label": "Label Help",
- "oldfieldtype": "HTML"
- },
- {
- "fieldname": "fieldname",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Fieldname",
- "no_copy": 1,
- "oldfieldname": "fieldname",
- "oldfieldtype": "Data",
- "read_only": 1
- },
- {
- "description": "Select the label after which you want to insert new field.",
- "fieldname": "insert_after",
- "fieldtype": "Select",
- "label": "Insert After",
- "no_copy": 1,
- "oldfieldname": "insert_after",
- "oldfieldtype": "Select"
- },
- {
- "fieldname": "column_break_6",
- "fieldtype": "Column Break"
- },
- {
- "bold": 1,
- "default": "Data",
- "fieldname": "fieldtype",
- "fieldtype": "Select",
- "in_filter": 1,
- "in_list_view": 1,
- "label": "Field Type",
- "oldfieldname": "fieldtype",
- "oldfieldtype": "Select",
- "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
- "reqd": 1
- },
- {
- "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
- "description": "Set non-standard precision for a Float or Currency field",
- "fieldname": "precision",
- "fieldtype": "Select",
- "label": "Precision",
- "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
- },
- {
- "fieldname": "options",
- "fieldtype": "Small Text",
- "in_list_view": 1,
- "label": "Options",
- "oldfieldname": "options",
- "oldfieldtype": "Text"
- },
- {
- "fieldname": "fetch_from",
- "fieldtype": "Small Text",
- "label": "Fetch From"
- },
- {
- "default": "0",
- "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
- "fieldname": "fetch_if_empty",
- "fieldtype": "Check",
- "label": "Fetch If Empty"
- },
- {
- "fieldname": "options_help",
- "fieldtype": "HTML",
- "label": "Options Help",
- "oldfieldtype": "HTML"
- },
- {
- "fieldname": "section_break_11",
- "fieldtype": "Section Break"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype==\"Section Break\"",
- "fieldname": "collapsible",
- "fieldtype": "Check",
- "label": "Collapsible"
- },
- {
- "depends_on": "eval:doc.fieldtype==\"Section Break\"",
- "fieldname": "collapsible_depends_on",
- "fieldtype": "Code",
- "label": "Collapsible Depends On"
- },
- {
- "fieldname": "default",
- "fieldtype": "Text",
- "label": "Default Value",
- "oldfieldname": "default",
- "oldfieldtype": "Text"
- },
- {
- "fieldname": "depends_on",
- "fieldtype": "Code",
- "label": "Depends On",
- "length": 255
- },
- {
- "fieldname": "description",
- "fieldtype": "Text",
- "label": "Field Description",
- "oldfieldname": "description",
- "oldfieldtype": "Text",
- "print_width": "300px",
- "width": "300px"
- },
- {
- "default": "0",
- "fieldname": "permlevel",
- "fieldtype": "Int",
- "label": "Permission Level",
- "oldfieldname": "permlevel",
- "oldfieldtype": "Int"
- },
- {
- "fieldname": "width",
- "fieldtype": "Data",
- "label": "Width",
- "oldfieldname": "width",
- "oldfieldtype": "Data"
- },
- {
- "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
- "fieldname": "columns",
- "fieldtype": "Int",
- "label": "Columns"
- },
- {
- "fieldname": "properties",
- "fieldtype": "Column Break",
- "oldfieldtype": "Column Break",
- "print_width": "50%",
- "width": "50%"
- },
- {
- "default": "0",
- "fieldname": "reqd",
- "fieldtype": "Check",
- "in_list_view": 1,
- "label": "Is Mandatory Field",
- "oldfieldname": "reqd",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "unique",
- "fieldtype": "Check",
- "label": "Unique"
- },
- {
- "default": "0",
- "fieldname": "read_only",
- "fieldtype": "Check",
- "label": "Read Only"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype===\"Link\"",
- "fieldname": "ignore_user_permissions",
- "fieldtype": "Check",
- "label": "Ignore User Permissions"
- },
- {
- "default": "0",
- "fieldname": "hidden",
- "fieldtype": "Check",
- "label": "Hidden"
- },
- {
- "default": "0",
- "fieldname": "print_hide",
- "fieldtype": "Check",
- "label": "Print Hide",
- "oldfieldname": "print_hide",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
- "fieldname": "print_hide_if_no_value",
- "fieldtype": "Check",
- "label": "Print Hide If No Value"
- },
- {
- "fieldname": "print_width",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Print Width",
- "no_copy": 1,
- "print_hide": 1
- },
- {
- "default": "0",
- "fieldname": "no_copy",
- "fieldtype": "Check",
- "label": "No Copy",
- "oldfieldname": "no_copy",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "allow_on_submit",
- "fieldtype": "Check",
- "label": "Allow on Submit",
- "oldfieldname": "allow_on_submit",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "in_list_view",
- "fieldtype": "Check",
- "label": "In List View"
- },
- {
- "default": "0",
- "fieldname": "in_standard_filter",
- "fieldtype": "Check",
- "label": "In Standard Filter"
- },
- {
- "default": "0",
- "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
- "fieldname": "in_global_search",
- "fieldtype": "Check",
- "label": "In Global Search"
- },
- {
- "default": "0",
- "fieldname": "bold",
- "fieldtype": "Check",
- "label": "Bold"
- },
- {
- "default": "0",
- "fieldname": "report_hide",
- "fieldtype": "Check",
- "label": "Report Hide",
- "oldfieldname": "report_hide",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "search_index",
- "fieldtype": "Check",
- "hidden": 1,
- "label": "Index",
- "no_copy": 1,
- "print_hide": 1
- },
- {
- "default": "0",
- "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
- "fieldname": "ignore_xss_filter",
- "fieldtype": "Check",
- "label": "Ignore XSS Filter"
- },
- {
- "default": "1",
- "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
- "fieldname": "translatable",
- "fieldtype": "Check",
- "label": "Translatable"
- },
- {
- "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
- "fieldname": "length",
- "fieldtype": "Int",
- "label": "Length"
- },
- {
- "fieldname": "mandatory_depends_on",
- "fieldtype": "Code",
- "label": "Mandatory Depends On",
- "length": 255
- },
- {
- "fieldname": "read_only_depends_on",
- "fieldtype": "Code",
- "label": "Read Only Depends On",
- "length": 255
- },
- {
- "default": "0",
- "fieldname": "allow_in_quick_entry",
- "fieldtype": "Check",
- "label": "Allow in Quick Entry"
- },
- {
- "default": "0",
- "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
- "fieldname": "in_preview",
- "fieldtype": "Check",
- "label": "In Preview"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype=='Duration'",
- "fieldname": "hide_seconds",
- "fieldtype": "Check",
- "label": "Hide Seconds"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype=='Duration'",
- "fieldname": "hide_days",
- "fieldtype": "Check",
- "label": "Hide Days"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype=='Section Break'",
- "fieldname": "hide_border",
- "fieldtype": "Check",
- "label": "Hide Border"
- },
- {
- "default": "0",
- "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
- "fieldname": "non_negative",
- "fieldtype": "Check",
- "label": "Non Negative"
- }
- ],
- "icon": "fa fa-glass",
- "idx": 1,
- "index_web_pages_for_search": 1,
- "links": [],
- "modified": "2020-10-29 06:14:43.073329",
- "modified_by": "Administrator",
- "module": "Custom",
- "name": "Custom Field",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Administrator",
- "share": 1,
- "write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "search_fields": "dt,label,fieldtype,options",
- "sort_field": "modified",
- "sort_order": "ASC",
- "track_changes": 1
+ "actions": [],
+ "allow_import": 1,
+ "creation": "2013-01-10 16:34:01",
+ "description": "Adds a custom field to a DocType",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "dt",
+ "module",
+ "label",
+ "label_help",
+ "fieldname",
+ "insert_after",
+ "length",
+ "column_break_6",
+ "fieldtype",
+ "precision",
+ "hide_seconds",
+ "hide_days",
+ "options",
+ "fetch_from",
+ "fetch_if_empty",
+ "options_help",
+ "section_break_11",
+ "collapsible",
+ "collapsible_depends_on",
+ "default",
+ "depends_on",
+ "mandatory_depends_on",
+ "read_only_depends_on",
+ "properties",
+ "non_negative",
+ "reqd",
+ "unique",
+ "read_only",
+ "ignore_user_permissions",
+ "hidden",
+ "print_hide",
+ "print_hide_if_no_value",
+ "print_width",
+ "no_copy",
+ "allow_on_submit",
+ "in_list_view",
+ "in_standard_filter",
+ "in_global_search",
+ "in_preview",
+ "bold",
+ "report_hide",
+ "search_index",
+ "allow_in_quick_entry",
+ "ignore_xss_filter",
+ "translatable",
+ "hide_border",
+ "description",
+ "permlevel",
+ "width",
+ "columns"
+ ],
+ "fields": [{
+ "bold": 1,
+ "fieldname": "dt",
+ "fieldtype": "Link",
+ "in_filter": 1,
+ "in_list_view": 1,
+ "label": "Document",
+ "oldfieldname": "dt",
+ "oldfieldtype": "Link",
+ "options": "DocType",
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "bold": 1,
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "in_filter": 1,
+ "label": "Label",
+ "no_copy": 1,
+ "oldfieldname": "label",
+ "oldfieldtype": "Data"
+ },
+ {
+ "fieldname": "label_help",
+ "fieldtype": "HTML",
+ "label": "Label Help",
+ "oldfieldtype": "HTML"
+ },
+ {
+ "fieldname": "fieldname",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Fieldname",
+ "no_copy": 1,
+ "oldfieldname": "fieldname",
+ "oldfieldtype": "Data",
+ "read_only": 1
+ },
+ {
+ "description": "Select the label after which you want to insert new field.",
+ "fieldname": "insert_after",
+ "fieldtype": "Select",
+ "label": "Insert After",
+ "no_copy": 1,
+ "oldfieldname": "insert_after",
+ "oldfieldtype": "Select"
+ },
+ {
+ "fieldname": "column_break_6",
+ "fieldtype": "Column Break"
+ },
+ {
+ "bold": 1,
+ "default": "Data",
+ "fieldname": "fieldtype",
+ "fieldtype": "Select",
+ "in_filter": 1,
+ "in_list_view": 1,
+ "label": "Field Type",
+ "oldfieldname": "fieldtype",
+ "oldfieldtype": "Select",
+ "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break",
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
+ "description": "Set non-standard precision for a Float or Currency field",
+ "fieldname": "precision",
+ "fieldtype": "Select",
+ "label": "Precision",
+ "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
+ },
+ {
+ "fieldname": "options",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Options",
+ "oldfieldname": "options",
+ "oldfieldtype": "Text"
+ },
+ {
+ "fieldname": "fetch_from",
+ "fieldtype": "Small Text",
+ "label": "Fetch From"
+ },
+ {
+ "default": "0",
+ "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
+ "fieldname": "fetch_if_empty",
+ "fieldtype": "Check",
+ "label": "Fetch If Empty"
+ },
+ {
+ "fieldname": "options_help",
+ "fieldtype": "HTML",
+ "label": "Options Help",
+ "oldfieldtype": "HTML"
+ },
+ {
+ "fieldname": "section_break_11",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype==\"Section Break\"",
+ "fieldname": "collapsible",
+ "fieldtype": "Check",
+ "label": "Collapsible"
+ },
+ {
+ "depends_on": "eval:doc.fieldtype==\"Section Break\"",
+ "fieldname": "collapsible_depends_on",
+ "fieldtype": "Code",
+ "label": "Collapsible Depends On"
+ },
+ {
+ "fieldname": "default",
+ "fieldtype": "Text",
+ "label": "Default Value",
+ "oldfieldname": "default",
+ "oldfieldtype": "Text"
+ },
+ {
+ "fieldname": "depends_on",
+ "fieldtype": "Code",
+ "label": "Depends On",
+ "length": 255
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "label": "Field Description",
+ "oldfieldname": "description",
+ "oldfieldtype": "Text",
+ "print_width": "300px",
+ "width": "300px"
+ },
+ {
+ "default": "0",
+ "fieldname": "permlevel",
+ "fieldtype": "Int",
+ "label": "Permission Level",
+ "oldfieldname": "permlevel",
+ "oldfieldtype": "Int"
+ },
+ {
+ "fieldname": "width",
+ "fieldtype": "Data",
+ "label": "Width",
+ "oldfieldname": "width",
+ "oldfieldtype": "Data"
+ },
+ {
+ "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
+ "fieldname": "columns",
+ "fieldtype": "Int",
+ "label": "Columns"
+ },
+ {
+ "fieldname": "properties",
+ "fieldtype": "Column Break",
+ "oldfieldtype": "Column Break",
+ "print_width": "50%",
+ "width": "50%"
+ },
+ {
+ "default": "0",
+ "fieldname": "reqd",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Is Mandatory Field",
+ "oldfieldname": "reqd",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "unique",
+ "fieldtype": "Check",
+ "label": "Unique"
+ },
+ {
+ "default": "0",
+ "fieldname": "read_only",
+ "fieldtype": "Check",
+ "label": "Read Only"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype===\"Link\"",
+ "fieldname": "ignore_user_permissions",
+ "fieldtype": "Check",
+ "label": "Ignore User Permissions"
+ },
+ {
+ "default": "0",
+ "fieldname": "hidden",
+ "fieldtype": "Check",
+ "label": "Hidden"
+ },
+ {
+ "default": "0",
+ "fieldname": "print_hide",
+ "fieldtype": "Check",
+ "label": "Print Hide",
+ "oldfieldname": "print_hide",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
+ "fieldname": "print_hide_if_no_value",
+ "fieldtype": "Check",
+ "label": "Print Hide If No Value"
+ },
+ {
+ "fieldname": "print_width",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Print Width",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "no_copy",
+ "fieldtype": "Check",
+ "label": "No Copy",
+ "oldfieldname": "no_copy",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_on_submit",
+ "fieldtype": "Check",
+ "label": "Allow on Submit",
+ "oldfieldname": "allow_on_submit",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "in_list_view",
+ "fieldtype": "Check",
+ "label": "In List View"
+ },
+ {
+ "default": "0",
+ "fieldname": "in_standard_filter",
+ "fieldtype": "Check",
+ "label": "In Standard Filter"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
+ "fieldname": "in_global_search",
+ "fieldtype": "Check",
+ "label": "In Global Search"
+ },
+ {
+ "default": "0",
+ "fieldname": "bold",
+ "fieldtype": "Check",
+ "label": "Bold"
+ },
+ {
+ "default": "0",
+ "fieldname": "report_hide",
+ "fieldtype": "Check",
+ "label": "Report Hide",
+ "oldfieldname": "report_hide",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "search_index",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Index",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "default": "0",
+ "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
+ "fieldname": "ignore_xss_filter",
+ "fieldtype": "Check",
+ "label": "Ignore XSS Filter"
+ },
+ {
+ "default": "1",
+ "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
+ "fieldname": "translatable",
+ "fieldtype": "Check",
+ "label": "Translatable"
+ },
+ {
+ "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
+ "fieldname": "length",
+ "fieldtype": "Int",
+ "label": "Length"
+ },
+ {
+ "fieldname": "mandatory_depends_on",
+ "fieldtype": "Code",
+ "label": "Mandatory Depends On",
+ "length": 255
+ },
+ {
+ "fieldname": "read_only_depends_on",
+ "fieldtype": "Code",
+ "label": "Read Only Depends On",
+ "length": 255
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_in_quick_entry",
+ "fieldtype": "Check",
+ "label": "Allow in Quick Entry"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
+ "fieldname": "in_preview",
+ "fieldtype": "Check",
+ "label": "In Preview"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_seconds",
+ "fieldtype": "Check",
+ "label": "Hide Seconds"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_days",
+ "fieldtype": "Check",
+ "label": "Hide Days"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Section Break'",
+ "fieldname": "hide_border",
+ "fieldtype": "Check",
+ "label": "Hide Border"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
+ "fieldname": "non_negative",
+ "fieldtype": "Check",
+ "label": "Non Negative"
+ },
+ {
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module (for export)",
+ "options": "Module Def"
+ }
+ ],
+ "icon": "fa fa-glass",
+ "idx": 1,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-09-04 12:45:23.810120",
+ "modified_by": "Administrator",
+ "module": "Custom",
+ "name": "Custom Field",
+ "owner": "Administrator",
+ "permissions": [{
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Administrator",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "search_fields": "dt,label,fieldtype,options",
+ "sort_field": "modified",
+ "sort_order": "ASC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py
index 7e6ea1875a..8c22d3c45c 100644
--- a/frappe/custom/doctype/custom_field/custom_field.py
+++ b/frappe/custom/doctype/custom_field/custom_field.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import json
@@ -18,7 +18,7 @@ class CustomField(Document):
if not self.fieldname:
label = self.label
if not label:
- if self.fieldtype in ["Section Break", "Column Break"]:
+ if self.fieldtype in ["Section Break", "Column Break", "Tab Break"]:
label = self.fieldtype + "_" + str(self.idx)
else:
frappe.throw(_("Label is mandatory"))
@@ -85,12 +85,10 @@ class CustomField(Document):
frappe.bold(self.label)))
# delete property setter entries
- frappe.db.sql("""\
- DELETE FROM `tabProperty Setter`
- WHERE doc_type = %s
- AND field_name = %s""",
- (self.dt, self.fieldname))
-
+ frappe.db.delete("Property Setter", {
+ "doc_type": self.dt,
+ "field_name": self.fieldname
+ })
frappe.clear_cache(doctype=self.dt)
def validate_insert_after(self, meta):
@@ -133,7 +131,7 @@ def create_custom_field(doctype, df, ignore_validate=False):
"permlevel": 0,
"fieldtype": 'Data',
"hidden": 0,
- # Looks like we always use this programatically?
+ # Looks like we always use this programatically?
# "is_standard": 1
})
custom_field.update(df)
@@ -148,24 +146,29 @@ def create_custom_fields(custom_fields, ignore_validate = False, update=True):
if not ignore_validate and frappe.flags.in_setup_wizard:
ignore_validate = True
- for doctype, fields in custom_fields.items():
+ for doctypes, fields in custom_fields.items():
if isinstance(fields, dict):
# only one field
fields = [fields]
- for df in fields:
- field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": df["fieldname"]})
- if not field:
- try:
- df["owner"] = "Administrator"
- create_custom_field(doctype, df, ignore_validate=ignore_validate)
- except frappe.exceptions.DuplicateEntryError:
- pass
- elif update:
- custom_field = frappe.get_doc("Custom Field", field)
- custom_field.flags.ignore_validate = ignore_validate
- custom_field.update(df)
- custom_field.save()
+ if isinstance(doctypes, str):
+ # only one doctype
+ doctypes = (doctypes,)
+
+ for doctype in doctypes:
+ for df in fields:
+ field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": df["fieldname"]})
+ if not field:
+ try:
+ df["owner"] = "Administrator"
+ create_custom_field(doctype, df, ignore_validate=ignore_validate)
+ except frappe.exceptions.DuplicateEntryError:
+ pass
+ elif update:
+ custom_field = frappe.get_doc("Custom Field", field)
+ custom_field.flags.ignore_validate = ignore_validate
+ custom_field.update(df)
+ custom_field.save()
frappe.clear_cache(doctype=doctype)
frappe.db.updatedb(doctype)
diff --git a/frappe/custom/doctype/custom_field/test_custom_field.py b/frappe/custom/doctype/custom_field/test_custom_field.py
index 3196b66ee8..ad3cf27eea 100644
--- a/frappe/custom/doctype/custom_field/test_custom_field.py
+++ b/frappe/custom/doctype/custom_field/test_custom_field.py
@@ -1,12 +1,47 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
-test_records = frappe.get_test_records('Custom Field')
+test_records = frappe.get_test_records("Custom Field")
+
class TestCustomField(unittest.TestCase):
- pass
+ def test_create_custom_fields(self):
+ from .custom_field import create_custom_fields
+
+ create_custom_fields(
+ {
+ "Address": [
+ {
+ "fieldname": "_test_custom_field_1",
+ "label": "_Test Custom Field 1",
+ "fieldtype": "Data",
+ "insert_after": "phone",
+ },
+ ],
+ ("Address", "Contact"): [
+ {
+ "fieldname": "_test_custom_field_2",
+ "label": "_Test Custom Field 2",
+ "fieldtype": "Data",
+ "insert_after": "phone",
+ },
+ ],
+ }
+ )
+
+ frappe.db.commit()
+
+ self.assertTrue(
+ frappe.db.exists("Custom Field", "Address-_test_custom_field_1")
+ )
+ self.assertTrue(
+ frappe.db.exists("Custom Field", "Address-_test_custom_field_2")
+ )
+ self.assertTrue(
+ frappe.db.exists("Custom Field", "Contact-_test_custom_field_2")
+ )
diff --git a/frappe/custom/doctype/customize_form/__init__.py b/frappe/custom/doctype/customize_form/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/custom/doctype/customize_form/__init__.py
+++ b/frappe/custom/doctype/customize_form/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json
index b9dde88126..c2940a92e3 100644
--- a/frappe/custom/doctype/customize_form/customize_form.json
+++ b/frappe/custom/doctype/customize_form/customize_form.json
@@ -31,7 +31,6 @@
"default_print_format",
"column_break_29",
"show_preview_popup",
- "image_view",
"email_settings_section",
"default_email_template",
"column_break_26",
@@ -109,13 +108,6 @@
"fieldtype": "Check",
"label": "Track Changes"
},
- {
- "default": "0",
- "depends_on": "eval: doc.image_field",
- "fieldname": "image_view",
- "fieldtype": "Check",
- "label": "Image View"
- },
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
@@ -296,7 +288,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-06-02 06:49:16.782806",
+ "modified": "2021-06-21 19:01:06.920663",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index 1b8977acc4..94f25a41aa 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -1,5 +1,5 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See LICENSE
"""
Customize Form is a Single DocType used to mask the Property Setter
@@ -18,10 +18,11 @@ from frappe.custom.doctype.property_setter.property_setter import delete_propert
from frappe.model.docfield import supports_translation
from frappe.core.doctype.doctype.doctype import validate_series
+
class CustomizeForm(Document):
def on_update(self):
- frappe.db.sql("delete from tabSingles where doctype='Customize Form'")
- frappe.db.sql("delete from `tabCustomize Form Field`")
+ frappe.db.delete("Singles", {"doctype": "Customize Form"})
+ frappe.db.delete("Customize Form Field")
@frappe.whitelist()
def fetch_to_customize(self):
@@ -192,6 +193,16 @@ class CustomizeForm(Document):
if prop == "fieldtype":
self.validate_fieldtype_change(df, meta_df[0].get(prop), df.get(prop))
+ elif prop == "length":
+ old_value_length = cint(meta_df[0].get(prop))
+ new_value_length = cint(df.get(prop))
+
+ if new_value_length and (old_value_length > new_value_length):
+ self.check_length_for_fieldtypes.append({'df': df, 'old_value': meta_df[0].get(prop)})
+ self.validate_fieldtype_length()
+ else:
+ self.flags.update_db = True
+
elif prop == "allow_on_submit" and df.get(prop):
if not frappe.db.get_value("DocField",
{"parent": self.doc_type, "fieldname": df.fieldname}, "allow_on_submit"):
diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py
index 58bdcf9a18..8a287b17e8 100644
--- a/frappe/custom/doctype/customize_form/test_customize_form.py
+++ b/frappe/custom/doctype/customize_form/test_customize_form.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe, unittest, json
from frappe.test_runner import make_test_records_for_doctype
@@ -188,6 +188,26 @@ class TestCustomizeForm(unittest.TestCase):
def test_core_doctype_customization(self):
self.assertRaises(frappe.ValidationError, self.get_customize_form, 'User')
+ def test_save_customization_length_field_property(self):
+ # Using Notification Log doctype as it doesn't have any other custom fields
+ d = self.get_customize_form("Notification Log")
+
+ document_name = d.get("fields", {"fieldname": "document_name"})[0]
+ document_name.length = 255
+ d.run_method("save_customization")
+
+ self.assertEqual(frappe.db.get_value("Property Setter",
+ {"doc_type": "Notification Log", "property": "length", "field_name": "document_name"}, "value"), '255')
+
+ self.assertTrue(d.flags.update_db)
+
+ length = frappe.db.sql("""SELECT character_maximum_length
+ FROM information_schema.columns
+ WHERE table_name = 'tabNotification Log'
+ AND column_name = 'document_name'""")[0][0]
+
+ self.assertEqual(length, 255)
+
def test_custom_link(self):
try:
# create a dummy doctype linked to Event
@@ -232,6 +252,32 @@ class TestCustomizeForm(unittest.TestCase):
testdt.delete()
testdt1.delete()
+ def test_custom_internal_links(self):
+ # add a custom internal link
+ frappe.clear_cache()
+ d = self.get_customize_form("User Group")
+
+ d.append('links', dict(link_doctype='User Group Member', parent_doctype='User',
+ link_fieldname='user', table_fieldname='user_group_members', group='Tests', custom=1))
+
+ d.run_method("save_customization")
+
+ frappe.clear_cache()
+ user_group = frappe.get_meta('User Group')
+
+ # check links exist
+ self.assertTrue([d.name for d in user_group.links if d.link_doctype == 'User Group Member'])
+ self.assertTrue([d.name for d in user_group.links if d.parent_doctype == 'User'])
+
+ # remove the link
+ d = self.get_customize_form("User Group")
+ d.links = []
+ d.run_method("save_customization")
+
+ frappe.clear_cache()
+ user_group = frappe.get_meta('Event')
+ self.assertFalse([d.name for d in (user_group.links or []) if d.link_doctype == 'User Group Member'])
+
def test_custom_action(self):
test_route = '/app/List/DocType'
diff --git a/frappe/custom/doctype/customize_form_field/__init__.py b/frappe/custom/doctype/customize_form_field/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/custom/doctype/customize_form_field/__init__.py
+++ b/frappe/custom/doctype/customize_form_field/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json
index 227114137c..986b99a7af 100644
--- a/frappe/custom/doctype/customize_form_field/customize_form_field.json
+++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json
@@ -82,7 +82,7 @@
"label": "Type",
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
- "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime",
+ "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nTab Break",
"reqd": 1,
"search_index": 1
},
@@ -428,7 +428,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-10-29 06:11:57.661039",
+ "modified": "2021-07-11 21:57:24.479749",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",
diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.py b/frappe/custom/doctype/customize_form_field/customize_form_field.py
index f288e70754..67563cf048 100644
--- a/frappe/custom/doctype/customize_form_field/customize_form_field.py
+++ b/frappe/custom/doctype/customize_form_field/customize_form_field.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.js b/frappe/custom/doctype/doctype_layout/doctype_layout.js
index 679330e065..533efea9b8 100644
--- a/frappe/custom/doctype/doctype_layout/doctype_layout.js
+++ b/frappe/custom/doctype/doctype_layout/doctype_layout.js
@@ -23,7 +23,7 @@ frappe.ui.form.on('DocType Layout', {
set_button(frm) {
if (!frm.is_new()) {
frm.add_custom_button(__('Go to {0} List', [frm.doc.name]), () => {
- window.open(`/app/list/${frappe.router.slug(frm.doc.name)}/list`);
+ window.open(`/app/${frappe.router.slug(frm.doc.name)}`);
});
}
}
diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.py b/frappe/custom/doctype/doctype_layout/doctype_layout.py
index 0dc320353d..fa285ddb62 100644
--- a/frappe/custom/doctype/doctype_layout/doctype_layout.py
+++ b/frappe/custom/doctype/doctype_layout/doctype_layout.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from frappe.model.document import Document
diff --git a/frappe/custom/doctype/doctype_layout/test_doctype_layout.py b/frappe/custom/doctype/doctype_layout/test_doctype_layout.py
index dcde3c00a4..a63dd7ee16 100644
--- a/frappe/custom/doctype/doctype_layout/test_doctype_layout.py
+++ b/frappe/custom/doctype/doctype_layout/test_doctype_layout.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.json b/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.json
index a1a36216c3..006c01ae4e 100644
--- a/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.json
+++ b/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.json
@@ -20,14 +20,13 @@
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
- "label": "Label",
- "reqd": 1
+ "label": "Label"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-11-16 17:13:01.892345",
+ "modified": "2021-05-19 16:27:40.585865",
"modified_by": "Administrator",
"module": "Custom",
"name": "DocType Layout Field",
@@ -36,4 +35,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py b/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py
index c1e963602f..3f8487b659 100644
--- a/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py
+++ b/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/custom/doctype/property_setter/__init__.py b/frappe/custom/doctype/property_setter/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/custom/doctype/property_setter/__init__.py
+++ b/frappe/custom/doctype/property_setter/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/custom/doctype/property_setter/property_setter.json b/frappe/custom/doctype/property_setter/property_setter.json
index b318d92c5a..fcb36637fe 100644
--- a/frappe/custom/doctype/property_setter/property_setter.json
+++ b/frappe/custom/doctype/property_setter/property_setter.json
@@ -13,6 +13,8 @@
"field_name",
"row_name",
"column_break0",
+ "module",
+ "section_break_9",
"property",
"property_type",
"value",
@@ -91,13 +93,23 @@
"fieldname": "row_name",
"fieldtype": "Data",
"label": "Row Name"
+ },
+ {
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module (for export)",
+ "options": "Module Def"
+ },
+ {
+ "fieldname": "section_break_9",
+ "fieldtype": "Section Break"
}
],
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-09-24 14:42:38.599684",
+ "modified": "2021-09-04 12:46:17.860769",
"modified_by": "Administrator",
"module": "Custom",
"name": "Property Setter",
diff --git a/frappe/custom/doctype/property_setter/property_setter.py b/frappe/custom/doctype/property_setter/property_setter.py
index 2a6c06b70a..7f40be9725 100644
--- a/frappe/custom/doctype/property_setter/property_setter.py
+++ b/frappe/custom/doctype/property_setter/property_setter.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
@@ -34,7 +34,7 @@ class PropertySetter(Document):
fields=['fieldname', 'label', 'fieldtype'],
filters={
'parent': dt,
- 'fieldtype': ['not in', ('Section Break', 'Column Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields],
+ 'fieldtype': ['not in', ('Section Break', 'Column Break', 'Tab Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields],
'fieldname': ['!=', '']
},
order_by='label asc',
@@ -43,20 +43,28 @@ class PropertySetter(Document):
def get_setup_data(self):
return {
- 'doctypes': [d[0] for d in frappe.db.sql("select name from tabDocType")],
+ 'doctypes': frappe.get_all("DocType", pluck="name"),
'dt_properties': self.get_property_list('DocType'),
'df_properties': self.get_property_list('DocField')
}
def get_field_ids(self):
- return frappe.db.sql("select name, fieldtype, label, fieldname from tabDocField where parent=%s", self.doc_type, as_dict = 1)
+ return frappe.db.get_values(
+ "DocField",
+ filters={"parent": self.doc_type},
+ fieldname=["name", "fieldtype", "label", "fieldname"],
+ as_dict=True,
+ )
def get_defaults(self):
if not self.field_name:
- return frappe.db.sql("select * from `tabDocType` where name=%s", self.doc_type, as_dict = 1)[0]
+ return frappe.get_all("DocType", filters={"name": self.doc_type}, fields="*")[0]
else:
- return frappe.db.sql("select * from `tabDocField` where fieldname=%s and parent=%s",
- (self.field_name, self.doc_type), as_dict = 1)[0]
+ return frappe.db.get_values(
+ "DocField",
+ filters={"fieldname": self.field_name, "parent": self.doc_type},
+ fieldname="*",
+ )[0]
def on_update(self):
if frappe.flags.in_patch:
diff --git a/frappe/custom/doctype/property_setter/test_property_setter.py b/frappe/custom/doctype/property_setter/test_property_setter.py
index 4d4de66d51..1bbbe59a0f 100644
--- a/frappe/custom/doctype/property_setter/test_property_setter.py
+++ b/frappe/custom/doctype/property_setter/test_property_setter.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/custom/doctype/test_rename_new/test_rename_new.py b/frappe/custom/doctype/test_rename_new/test_rename_new.py
index 32d2396b2b..fc4ab97cfe 100644
--- a/frappe/custom/doctype/test_rename_new/test_rename_new.py
+++ b/frappe/custom/doctype/test_rename_new/test_rename_new.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/custom/doctype/test_rename_new/test_test_rename_new.py b/frappe/custom/doctype/test_rename_new/test_test_rename_new.py
index b3ea4818de..03202669ed 100644
--- a/frappe/custom/doctype/test_rename_new/test_test_rename_new.py
+++ b/frappe/custom/doctype/test_rename_new/test_test_rename_new.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/custom/workspace/customization/customization.json b/frappe/custom/workspace/customization/customization.json
index cdc3b73366..7aec530604 100644
--- a/frappe/custom/workspace/customization/customization.json
+++ b/frappe/custom/workspace/customization/customization.json
@@ -1,23 +1,20 @@
{
- "category": "Administration",
"charts": [],
+ "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Customize Form\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Custom Role\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Client Script\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Server Script\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Dashboards\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Form Customization\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other\", \"col\": 4}}]",
"creation": "2020-03-02 15:15:03.839594",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends_another_page": 0,
+ "for_user": "",
"hide_custom": 0,
"icon": "customization",
"idx": 0,
- "is_default": 0,
- "is_standard": 1,
"label": "Customization",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Dashboards",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -26,6 +23,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Dashboard",
+ "link_count": 0,
"link_to": "Dashboard",
"link_type": "DocType",
"onboard": 0,
@@ -36,6 +34,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Dashboard Chart",
+ "link_count": 0,
"link_to": "Dashboard Chart",
"link_type": "DocType",
"onboard": 0,
@@ -46,6 +45,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Dashboard Chart Source",
+ "link_count": 0,
"link_to": "Dashboard Chart Source",
"link_type": "DocType",
"onboard": 0,
@@ -55,6 +55,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Form Customization",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -63,6 +64,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Customize Form",
+ "link_count": 0,
"link_to": "Customize Form",
"link_type": "DocType",
"onboard": 0,
@@ -73,6 +75,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Custom Field",
+ "link_count": 0,
"link_to": "Custom Field",
"link_type": "DocType",
"onboard": 0,
@@ -83,6 +86,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Client Script",
+ "link_count": 0,
"link_to": "Client Script",
"link_type": "DocType",
"onboard": 0,
@@ -93,6 +97,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "DocType",
+ "link_count": 0,
"link_to": "DocType",
"link_type": "DocType",
"onboard": 0,
@@ -102,6 +107,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Other",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -110,19 +116,23 @@
"hidden": 0,
"is_query_report": 0,
"label": "Custom Translations",
+ "link_count": 0,
"link_to": "Translation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
- "modified": "2021-02-04 13:50:35.750463",
+ "modified": "2021-08-05 12:15:57.486113",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customization",
"owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
+ "parent_page": "",
+ "public": 1,
+ "restrict_to_domain": "",
+ "roles": [],
+ "sequence_id": 8,
"shortcuts": [
{
"label": "Customize Form",
@@ -145,5 +155,6 @@
"link_to": "Server Script",
"type": "DocType"
}
- ]
+ ],
+ "title": "Customization"
}
\ No newline at end of file
diff --git a/frappe/data/google_fonts.json b/frappe/data/google_fonts.json
new file mode 100644
index 0000000000..232e509e77
--- /dev/null
+++ b/frappe/data/google_fonts.json
@@ -0,0 +1,56 @@
+[
+ "Alegreya Sans",
+ "Alegreya",
+ "Andada Pro",
+ "Anton",
+ "Archivo Narrow",
+ "Archivo",
+ "BioRhyme",
+ "Cardo",
+ "Chivo",
+ "Cormorant",
+ "Crimson Text",
+ "DM Sans",
+ "Eczar",
+ "Encode Sans",
+ "Epilogue ",
+ "Fira Sans",
+ "Hahmlet",
+ "IBM Plex Sans",
+ "Inconsolata",
+ "Inknut Antiqua",
+ "Inter",
+ "JetBrains Mono",
+ "Karla",
+ "Lato",
+ "Libre Baskerville",
+ "Libre Franklin",
+ "Lora",
+ "Manrope",
+ "Merriweather",
+ "Montserrat",
+ "Neuton",
+ "Nunito",
+ "Old Standard TT",
+ "Open Sans",
+ "Oswald",
+ "Oxygen",
+ "Playfair Display",
+ "Poppins",
+ "Proza Libre",
+ "PT Sans",
+ "PT Serif",
+ "Raleway",
+ "Roboto Slab",
+ "Roboto",
+ "Rubik",
+ "Sora",
+ "Source Sans Pro",
+ "Source Serif Pro",
+ "Space Grotesk",
+ "Space Mono",
+ "Spectral",
+ "Syne",
+ "Work Sans"
+]
+
diff --git a/frappe/data/sample_site_config.json b/frappe/data/sample_site_config.json
deleted file mode 100644
index 715cd7b9fa..0000000000
--- a/frappe/data/sample_site_config.json
+++ /dev/null
@@ -1,45 +0,0 @@
-{
- "db_name": "testdb",
- "db_password": "password",
- "mute_emails": true,
-
- "limits": {
- "emails": 1500,
- "space": 0.157,
- "expiry": "2016-07-25",
- "users": 1
- },
-
- "developer_mode": 1,
- "auto_cache_clear": true,
- "disable_website_cache": true,
- "max_file_size": 1000000,
-
- "mail_server": "localhost",
- "mail_login": null,
- "mail_password": null,
- "mail_port": 25,
- "use_ssl": 0,
- "auto_email_id": "hello@example.com",
-
- "google_analytics_id": "google_analytics_id",
- "google_analytics_anonymize_ip": 1,
-
- "google_login": {
- "client_id": "google_client_id",
- "client_secret": "google_client_secret"
- },
- "github_login": {
- "client_id": "github_client_id",
- "client_secret": "github_client_secret"
- },
- "facebook_login": {
- "client_id": "facebook_client_id",
- "client_secret": "facebook_client_secret"
- },
-
- "celery_broker": "redis://localhost",
- "celery_result_backend": null,
- "scheduler_interval": 300,
- "celery_queue_per_site": true
-}
diff --git a/frappe/data_migration/doctype/data_migration_connector/data_migration_connector.py b/frappe/data_migration/doctype/data_migration_connector/data_migration_connector.py
index d1137f2e67..2e4e4d45b3 100644
--- a/frappe/data_migration/doctype/data_migration_connector/data_migration_connector.py
+++ b/frappe/data_migration/doctype/data_migration_connector/data_migration_connector.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe, os
from frappe.model.document import Document
diff --git a/frappe/data_migration/doctype/data_migration_connector/test_data_migration_connector.py b/frappe/data_migration/doctype/data_migration_connector/test_data_migration_connector.py
index fd45f86ec1..ffc96c8266 100644
--- a/frappe/data_migration/doctype/data_migration_connector/test_data_migration_connector.py
+++ b/frappe/data_migration/doctype/data_migration_connector/test_data_migration_connector.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import unittest
class TestDataMigrationConnector(unittest.TestCase):
diff --git a/frappe/data_migration/doctype/data_migration_mapping/data_migration_mapping.py b/frappe/data_migration/doctype/data_migration_mapping/data_migration_mapping.py
index 5cb20ba56c..46d33eaca9 100644
--- a/frappe/data_migration/doctype/data_migration_mapping/data_migration_mapping.py
+++ b/frappe/data_migration/doctype/data_migration_mapping/data_migration_mapping.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/data_migration/doctype/data_migration_mapping/test_data_migration_mapping.py b/frappe/data_migration/doctype/data_migration_mapping/test_data_migration_mapping.py
index df11fc0522..b1040aaa58 100644
--- a/frappe/data_migration/doctype/data_migration_mapping/test_data_migration_mapping.py
+++ b/frappe/data_migration/doctype/data_migration_mapping/test_data_migration_mapping.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import unittest
class TestDataMigrationMapping(unittest.TestCase):
diff --git a/frappe/data_migration/doctype/data_migration_mapping_detail/data_migration_mapping_detail.py b/frappe/data_migration/doctype/data_migration_mapping_detail/data_migration_mapping_detail.py
index 6d3ef50937..ce46f60f67 100644
--- a/frappe/data_migration/doctype/data_migration_mapping_detail/data_migration_mapping_detail.py
+++ b/frappe/data_migration/doctype/data_migration_mapping_detail/data_migration_mapping_detail.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from frappe.model.document import Document
diff --git a/frappe/data_migration/doctype/data_migration_plan/data_migration_plan.py b/frappe/data_migration/doctype/data_migration_plan/data_migration_plan.py
index a8d0e40a4c..94ed77e2ec 100644
--- a/frappe/data_migration/doctype/data_migration_plan/data_migration_plan.py
+++ b/frappe/data_migration/doctype/data_migration_plan/data_migration_plan.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.modules import get_module_path, scrub_dt_dn
diff --git a/frappe/data_migration/doctype/data_migration_plan/test_data_migration_plan.py b/frappe/data_migration/doctype/data_migration_plan/test_data_migration_plan.py
index 14c585a82d..649f7db903 100644
--- a/frappe/data_migration/doctype/data_migration_plan/test_data_migration_plan.py
+++ b/frappe/data_migration/doctype/data_migration_plan/test_data_migration_plan.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import unittest
class TestDataMigrationPlan(unittest.TestCase):
diff --git a/frappe/data_migration/doctype/data_migration_plan_mapping/data_migration_plan_mapping.py b/frappe/data_migration/doctype/data_migration_plan_mapping/data_migration_plan_mapping.py
index ba4cf28eb8..7939a68d97 100644
--- a/frappe/data_migration/doctype/data_migration_plan_mapping/data_migration_plan_mapping.py
+++ b/frappe/data_migration/doctype/data_migration_plan_mapping/data_migration_plan_mapping.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from frappe.model.document import Document
diff --git a/frappe/data_migration/doctype/data_migration_run/data_migration_run.py b/frappe/data_migration/doctype/data_migration_run/data_migration_run.py
index c35af5827b..deb14baf27 100644
--- a/frappe/data_migration/doctype/data_migration_run/data_migration_run.py
+++ b/frappe/data_migration/doctype/data_migration_run/data_migration_run.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe, json, math
from frappe.model.document import Document
diff --git a/frappe/data_migration/doctype/data_migration_run/test_data_migration_run.py b/frappe/data_migration/doctype/data_migration_run/test_data_migration_run.py
index ef7b70dca2..485f86a7f9 100644
--- a/frappe/data_migration/doctype/data_migration_run/test_data_migration_run.py
+++ b/frappe/data_migration/doctype/data_migration_run/test_data_migration_run.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe, unittest
class TestDataMigrationRun(unittest.TestCase):
diff --git a/frappe/database/__init__.py b/frappe/database/__init__.py
index a899bec3d1..b0e3183d4f 100644
--- a/frappe/database/__init__.py
+++ b/frappe/database/__init__.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
# Database Module
# --------------------
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 81e24cc7ad..a7dd9b6b66 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -1,11 +1,12 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
# Database Module
# --------------------
import re
import time
+from typing import Dict, List, Union
import frappe
import datetime
import frappe.defaults
@@ -13,9 +14,13 @@ import frappe.model.meta
from frappe import _
from time import time
-from frappe.utils import now, getdate, cast_fieldtype, get_datetime
+from frappe.utils import now, getdate, cast, get_datetime
from frappe.model.utils.link_count import flush_local_link_count
-from frappe.utils import cint
+from frappe.query_builder.functions import Count
+from frappe.query_builder.functions import Min, Max, Avg, Sum
+from frappe.query_builder.utils import Column
+from .query import Query
+from pypika.terms import Criterion, PseudoColumn
class Database(object):
@@ -32,6 +37,7 @@ class Database(object):
STANDARD_VARCHAR_COLUMNS = ('name', 'owner', 'modified_by', 'parent', 'parentfield', 'parenttype')
DEFAULT_COLUMNS = ['name', 'creation', 'modified', 'modified_by', 'owner', 'docstatus', 'parent',
'parentfield', 'parenttype', 'idx']
+ MAX_WRITES_PER_TRANSACTION = 200_000
class InvalidColumnName(frappe.ValidationError): pass
@@ -55,6 +61,7 @@ class Database(object):
self.password = password or frappe.conf.db_password
self.value_cache = {}
+ self.query = Query()
def setup_type_map(self):
pass
@@ -77,7 +84,8 @@ class Database(object):
pass
def sql(self, query, values=(), as_dict = 0, as_list = 0, formatted = 0,
- debug=0, ignore_ddl=0, as_utf8=0, auto_commit=0, update=None, explain=False):
+ debug=0, ignore_ddl=0, as_utf8=0, auto_commit=0, update=None,
+ explain=False, run=True, pluck=False):
"""Execute a SQL query and fetch all rows.
:param query: SQL query.
@@ -90,7 +98,7 @@ class Database(object):
:param as_utf8: Encode values as UTF 8.
:param auto_commit: Commit after executing the query.
:param update: Update this dict to all rows (if returned `as_dict`).
-
+ :param run: Returns query without executing it if False.
Examples:
# return customer names as dicts
@@ -104,6 +112,10 @@ class Database(object):
{"name": "a%", "owner":"test@example.com"})
"""
+ query = str(query)
+ if not run:
+ return query
+
if re.search(r'ifnull\(', query, flags=re.IGNORECASE):
# replaces ifnull in query with coalesce
query = re.sub(r'ifnull\(', 'coalesce(', query, flags=re.IGNORECASE)
@@ -158,6 +170,12 @@ class Database(object):
frappe.errprint('Syntax error in query:')
frappe.errprint(query)
+ elif self.is_deadlocked(e):
+ raise frappe.QueryDeadlockError
+
+ elif self.is_timedout(e):
+ raise frappe.QueryTimeoutError
+
if ignore_ddl and (self.is_missing_column(e) or self.is_missing_table(e) or self.cant_drop_field_or_key(e)):
pass
else:
@@ -168,6 +186,9 @@ class Database(object):
if not self._cursor.description:
return ()
+ if pluck:
+ return [r[0] for r in self._cursor.fetchall()]
+
# scrub output if required
if as_dict:
ret = self.fetch_as_dict(formatted, as_utf8)
@@ -223,7 +244,7 @@ class Database(object):
except Exception:
frappe.errprint("error in query explain")
- def sql_list(self, query, values=(), debug=False):
+ def sql_list(self, query, values=(), debug=False, **kwargs):
"""Return data as list of single elements (first column).
Example:
@@ -231,7 +252,7 @@ class Database(object):
# doctypes = ["DocType", "DocField", "User", ...]
doctypes = frappe.db.sql_list("select name from DocType")
"""
- return [r[0] for r in self.sql(query, values, debug=debug)]
+ return [r[0] for r in self.sql(query, values, **kwargs, debug=debug)]
def sql_ddl(self, query, values=(), debug=False):
"""Commit and execute a query. DDL (Data Definition Language) queries that alter schema
@@ -252,7 +273,7 @@ class Database(object):
if query[:6].lower() in ('update', 'insert', 'delete'):
self.transaction_writes += 1
- if self.transaction_writes > 200000:
+ if self.transaction_writes > self.MAX_WRITES_PER_TRANSACTION:
if self.auto_commit_on_many_writes:
self.commit()
else:
@@ -309,65 +330,12 @@ class Database(object):
nres.append(nr)
return nres
- def build_conditions(self, filters):
- """Convert filters sent as dict, lists to SQL conditions. filter's key
- is passed by map function, build conditions like:
-
- * ifnull(`fieldname`, default_value) = %(fieldname)s
- * `fieldname` [=, !=, >, >=, <, <=] %(fieldname)s
- """
- conditions = []
- values = {}
- def _build_condition(key):
- """
- filter's key is passed by map function
- build conditions like:
- * ifnull(`fieldname`, default_value) = %(fieldname)s
- * `fieldname` [=, !=, >, >=, <, <=] %(fieldname)s
- """
- _operator = "="
- _rhs = " %(" + key + ")s"
- value = filters.get(key)
- values[key] = value
- if isinstance(value, (list, tuple)):
- # value is a tuple like ("!=", 0)
- _operator = value[0]
- values[key] = value[1]
- if isinstance(value[1], (tuple, list)):
- # value is a list in tuple ("in", ("A", "B"))
- _rhs = " ({0})".format(", ".join(self.escape(v) for v in value[1]))
- del values[key]
-
- if _operator not in ["=", "!=", ">", ">=", "<", "<=", "like", "in", "not in", "not like"]:
- _operator = "="
-
- if "[" in key:
- split_key = key.split("[")
- condition = "coalesce(`" + split_key[0] + "`, " + split_key[1][:-1] + ") " \
- + _operator + _rhs
- else:
- condition = "`" + key + "` " + _operator + _rhs
-
- conditions.append(condition)
-
- if isinstance(filters, int):
- # docname is a number, convert to string
- filters = str(filters)
-
- if isinstance(filters, str):
- filters = { "name": filters }
-
- for f in filters:
- _build_condition(f)
-
- return " and ".join(conditions), values
-
def get(self, doctype, filters=None, as_dict=True, cache=False):
"""Returns `get_value` with fieldname='*'"""
return self.get_value(doctype, filters, "*", as_dict=as_dict, cache=cache)
def get_value(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
- debug=False, order_by=None, cache=False, for_update=False):
+ debug=False, order_by=None, cache=False, for_update=False, run=True):
"""Returns a document property or list of properties.
:param doctype: DocType name.
@@ -394,12 +362,15 @@ class Database(object):
"""
ret = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug,
- order_by, cache=cache, for_update=for_update)
+ order_by, cache=cache, for_update=for_update, run=run)
+
+ if not run:
+ return ret
return ((len(ret[0]) > 1 or as_dict) and ret[0] or ret[0][0]) if ret else None
def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
- debug=False, order_by=None, update=None, cache=False, for_update=False):
+ debug=False, order_by=None, update=None, cache=False, for_update=False, run=True):
"""Returns multiple document properties.
:param doctype: DocType name.
@@ -423,10 +394,9 @@ class Database(object):
(doctype, filters, fieldname) in self.value_cache:
return self.value_cache[(doctype, filters, fieldname)]
- if not order_by: order_by = 'modified desc'
-
if isinstance(filters, list):
- out = self._get_value_for_many_names(doctype, filters, fieldname, debug=debug)
+ order_by = order_by or "modified_desc"
+ out = self._get_value_for_many_names(doctype, filters, fieldname, debug=debug, run=run)
else:
fields = fieldname
@@ -438,26 +408,29 @@ class Database(object):
if (filters is not None) and (filters!=doctype or doctype=="DocType"):
try:
- out = self._get_values_from_table(fields, filters, doctype, as_dict, debug, order_by, update, for_update=for_update)
+ order_by = order_by or "modified"
+ out = self._get_values_from_table(
+ fields, filters, doctype, as_dict, debug, order_by, update, for_update=for_update, run=run
+ )
except Exception as e:
if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)):
# table or column not found, return None
out = None
elif (not ignore) and frappe.db.is_table_missing(e):
# table not found, look in singles
- out = self.get_values_from_single(fields, filters, doctype, as_dict, debug, update)
+ out = self.get_values_from_single(fields, filters, doctype, as_dict, debug, update, run=run)
else:
raise
else:
- out = self.get_values_from_single(fields, filters, doctype, as_dict, debug, update)
+ out = self.get_values_from_single(fields, filters, doctype, as_dict, debug, update, run=run)
if cache and isinstance(filters, str):
self.value_cache[(doctype, filters, fieldname)] = out
return out
- def get_values_from_single(self, fields, filters, doctype, as_dict=False, debug=False, update=None):
+ def get_values_from_single(self, fields, filters, doctype, as_dict=False, debug=False, update=None, run=True):
"""Get values from `tabSingles` (Single DocTypes) (internal).
:param fields: List of fields,
@@ -486,8 +459,9 @@ class Database(object):
r = self.sql("""select field, value
from `tabSingles` where field in (%s) and doctype=%s"""
% (', '.join(['%s'] * len(fields)), '%s'),
- tuple(fields) + (doctype,), as_dict=False, debug=debug)
-
+ tuple(fields) + (doctype,), as_dict=False, debug=debug, run=run)
+ if not run:
+ return r
if as_dict:
if r:
r = frappe._dict(r)
@@ -515,7 +489,6 @@ class Database(object):
FROM `tabSingles`
WHERE doctype = %s
""", doctype)
- # result = _cast_result(doctype, result)
dict_ = frappe._dict(result)
@@ -542,7 +515,7 @@ class Database(object):
"""
if not doctype in self.value_cache:
- self.value_cache = self.value_cache[doctype] = {}
+ self.value_cache[doctype] = {}
if fieldname in self.value_cache[doctype]:
return self.value_cache[doctype][fieldname]
@@ -556,8 +529,7 @@ class Database(object):
if not df:
frappe.throw(_('Invalid field name: {0}').format(frappe.bold(fieldname)), self.InvalidColumnName)
- if df.fieldtype in frappe.model.numeric_fieldtypes:
- val = cint(val)
+ val = cast(df.fieldtype, val)
self.value_cache[doctype][fieldname] = val
@@ -567,44 +539,41 @@ class Database(object):
"""Alias for get_single_value"""
return self.get_single_value(*args, **kwargs)
- def _get_values_from_table(self, fields, filters, doctype, as_dict, debug, order_by=None, update=None, for_update=False):
- fl = []
- if isinstance(fields, (list, tuple)):
- for f in fields:
- if "(" in f or " as " in f: # function
- fl.append(f)
+ def _get_values_from_table(self, fields, filters, doctype, as_dict, debug, order_by=None,
+ update=None, for_update=False, run=True):
+ field_objects = []
+
+ if not isinstance(fields, Criterion):
+ for field in fields:
+ if "(" in field or " as " in field:
+ field_objects.append(PseudoColumn(field))
else:
- fl.append("`" + f + "`")
- fl = ", ".join(fl)
+ field_objects.append(field)
+
+ criterion = self.query.build_conditions(
+ table=doctype, filters=filters, orderby=order_by, for_update=for_update
+ )
+ if isinstance(fields, (list, tuple)):
+ query = criterion.select(*field_objects)
+
+ elif isinstance(fields, Criterion):
+ query = criterion.select(fields)
+
else:
- fl = fields
if fields=="*":
+ query = criterion.select(fields)
as_dict = True
-
- conditions, values = self.build_conditions(filters)
-
- order_by = ("order by " + order_by) if order_by else ""
-
- r = self.sql("select {fields} from `tab{doctype}` {where} {conditions} {order_by} {for_update}"
- .format(
- for_update = 'for update' if for_update else '',
- fields = fl,
- doctype = doctype,
- where = "where" if conditions else "",
- conditions = conditions,
- order_by = order_by),
- values, as_dict=as_dict, debug=debug, update=update)
-
+ r = self.sql(query, as_dict=as_dict, debug=debug, update=update, run=run)
return r
- def _get_value_for_many_names(self, doctype, names, field, debug=False):
+ def _get_value_for_many_names(self, doctype, names, field, debug=False, run=True):
names = list(filter(None, names))
if names:
return self.get_all(doctype,
fields=['name', field],
filters=[['name', 'in', names]],
- debug=debug, as_list=1)
+ debug=debug, as_list=1, run=run)
else:
return {}
@@ -649,7 +618,7 @@ class Database(object):
for key in to_update:
set_values.append('`{0}`=%({0})s'.format(key))
- for name in self.get_values(dt, dn, 'name', for_update=for_update):
+ for name in self.get_values(dt, dn, 'name', for_update=for_update, debug=debug):
values = dict(name=name[0])
values.update(to_update)
@@ -820,24 +789,32 @@ class Database(object):
except Exception:
return None
+ def min(self, dt, fieldname, filters=None, **kwargs):
+ return self.query.build_conditions(dt, filters=filters).select(Min(Column(fieldname))).run(**kwargs)[0][0] or 0
+
+ def max(self, dt, fieldname, filters=None, **kwargs):
+ return self.query.build_conditions(dt, filters=filters).select(Max(Column(fieldname))).run(**kwargs)[0][0] or 0
+
+ def avg(self, dt, fieldname, filters=None, **kwargs):
+ return self.query.build_conditions(dt, filters=filters).select(Avg(Column(fieldname))).run(**kwargs)[0][0] or 0
+
+ def sum(self, dt, fieldname, filters=None, **kwargs):
+ return self.query.build_conditions(dt, filters=filters).select(Sum(Column(fieldname))).run(**kwargs)[0][0] or 0
+
def count(self, dt, filters=None, debug=False, cache=False):
"""Returns `COUNT(*)` for given DocType and filters."""
if cache and not filters:
cache_count = frappe.cache().get_value('doctype:count:{}'.format(dt))
if cache_count is not None:
return cache_count
+ query = self.query.build_conditions(table=dt, filters=filters).select(Count("*"))
if filters:
- conditions, filters = self.build_conditions(filters)
- count = self.sql("""select count(*)
- from `tab%s` where %s""" % (dt, conditions), filters, debug=debug)[0][0]
+ count = self.sql(query, debug=debug)[0][0]
return count
else:
- count = self.sql("""select count(*)
- from `tab%s`""" % (dt,))[0][0]
-
+ count = self.sql(query, debug=debug)[0][0]
if cache:
frappe.cache().set_value('doctype:count:{}'.format(dt), count, expires_in_sec = 86400)
-
return count
@staticmethod
@@ -896,13 +873,13 @@ class Database(object):
WHERE table_name = 'tab{0}' AND column_name = '{1}' '''.format(doctype, column))[0][0]
def has_index(self, table_name, index_name):
- pass
+ raise NotImplementedError
def add_index(self, doctype, fields, index_name=None):
- pass
+ raise NotImplementedError
def add_unique(self, doctype, fields, constraint_name=None):
- pass
+ raise NotImplementedError
@staticmethod
def get_index_name(fields):
@@ -928,7 +905,7 @@ class Database(object):
def escape(s, percent=True):
"""Excape quotes and percent in given string."""
# implemented in specific class
- pass
+ raise NotImplementedError
@staticmethod
def is_column_missing(e):
@@ -953,15 +930,30 @@ class Database(object):
query = sql_dict.get(current_dialect)
return self.sql(query, values, **kwargs)
- def delete(self, doctype, conditions, debug=False):
- if conditions:
- conditions, values = self.build_conditions(conditions)
- return self.sql("DELETE FROM `tab{doctype}` where {conditions}".format(
- doctype=doctype,
- conditions=conditions
- ), values, debug=debug)
- else:
- frappe.throw(_('No conditions provided'))
+ def delete(self, doctype: str, filters: Union[Dict, List] = None, debug=False, **kwargs):
+ """Delete rows from a table in site which match the passed filters. This
+ does trigger DocType hooks. Simply runs a DELETE query in the database.
+
+ Doctype name can be passed directly, it will be pre-pended with `tab`.
+ """
+ values = ()
+ filters = filters or kwargs.get("conditions")
+ query = self.query.build_conditions(table=doctype, filters=filters).delete()
+ if "debug" not in kwargs:
+ kwargs["debug"] = debug
+ return self.sql(query, values, **kwargs)
+
+ def truncate(self, doctype: str):
+ """Truncate a table in the database. This runs a DDL command `TRUNCATE TABLE`.
+ This cannot be rolled back.
+
+ Doctype name can be passed directly, it will be pre-pended with `tab`.
+ """
+ table = doctype if doctype.startswith("__") else f"tab{doctype}"
+ return self.sql_ddl(f"truncate `{table}`")
+
+ def clear_table(self, doctype):
+ return self.truncate(doctype)
def get_last_created(self, doctype):
last_record = self.get_all(doctype, ('creation'), limit=1, order_by='creation desc')
@@ -970,9 +962,6 @@ class Database(object):
else:
return None
- def clear_table(self, doctype):
- self.sql('truncate `tab{}`'.format(doctype))
-
def log_touched_tables(self, query, values=None):
if values:
query = frappe.safe_decode(self._cursor.mogrify(query, values))
@@ -1023,6 +1012,7 @@ class Database(object):
), tuple(insert_list))
insert_list = []
+
def enqueue_jobs_after_commit():
from frappe.utils.background_jobs import execute_job, get_queue
@@ -1032,19 +1022,3 @@ def enqueue_jobs_after_commit():
q.enqueue_call(execute_job, timeout=job.get("timeout"),
kwargs=job.get("queue_args"))
frappe.flags.enqueue_after_commit = []
-
-# Helpers
-def _cast_result(doctype, result):
- batch = [ ]
-
- try:
- for field, value in result:
- df = frappe.get_meta(doctype).get_field(field)
- if df:
- value = cast_fieldtype(df.fieldtype, value)
-
- batch.append(tuple([field, value]))
- except frappe.exceptions.DoesNotExistError:
- return result
-
- return tuple(batch)
diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py
index 879c8394d7..2f6d640743 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -1,3 +1,5 @@
+from typing import List, Tuple, Union
+
import pymysql
from pymysql.constants import ER, FIELD_TYPE
from pymysql.converters import conversions, escape_string
@@ -5,7 +7,7 @@ from pymysql.converters import conversions, escape_string
import frappe
from frappe.database.database import Database
from frappe.database.mariadb.schema import MariaDBTable
-from frappe.utils import UnicodeWithAttrs, cstr, get_datetime
+from frappe.utils import UnicodeWithAttrs, cstr, get_datetime, get_table_name
class MariaDBDatabase(Database):
@@ -20,11 +22,11 @@ class MariaDBDatabase(Database):
def setup_type_map(self):
self.db_type = 'mariadb'
self.type_map = {
- 'Currency': ('decimal', '18,6'),
+ 'Currency': ('decimal', '21,9'),
'Int': ('int', '11'),
'Long Int': ('bigint', '20'),
- 'Float': ('decimal', '18,6'),
- 'Percent': ('decimal', '18,6'),
+ 'Float': ('decimal', '21,9'),
+ 'Percent': ('decimal', '21,9'),
'Check': ('int', '1'),
'Small Text': ('text', ''),
'Long Text': ('longtext', ''),
@@ -49,7 +51,8 @@ class MariaDBDatabase(Database):
'Color': ('varchar', self.VARCHAR_LEN),
'Barcode': ('longtext', ''),
'Geolocation': ('longtext', ''),
- 'Duration': ('decimal', '18,6')
+ 'Duration': ('decimal', '21,9'),
+ 'Icon': ('varchar', self.VARCHAR_LEN)
}
def get_connection(self):
@@ -123,6 +126,19 @@ class MariaDBDatabase(Database):
def is_type_datetime(code):
return code in (pymysql.DATE, pymysql.DATETIME)
+ def rename_table(self, old_name: str, new_name: str) -> Union[List, Tuple]:
+ old_name = get_table_name(old_name)
+ new_name = get_table_name(new_name)
+ return self.sql(f"RENAME TABLE `{old_name}` TO `{new_name}`")
+
+ def describe(self, doctype: str) -> Union[List, Tuple]:
+ table_name = get_table_name(doctype)
+ return self.sql(f"DESC `{table_name}`")
+
+ def change_column_type(self, doctype: str, column: str, type: str) -> Union[List, Tuple]:
+ table_name = get_table_name(doctype)
+ return self.sql(f"ALTER TABLE `{table_name}` MODIFY `{column}` {type} NOT NULL")
+
# exception types
@staticmethod
def is_deadlocked(e):
@@ -179,7 +195,7 @@ class MariaDBDatabase(Database):
`password` TEXT NOT NULL,
`encrypted` INT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`doctype`, `name`, `fieldname`)
- ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci""")
+ ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci""")
def create_global_search_table(self):
if not '__global_search' in self.get_tables():
@@ -240,11 +256,11 @@ class MariaDBDatabase(Database):
index_name=index_name
))
- def add_index(self, doctype, fields, index_name=None):
+ def add_index(self, doctype: str, fields: List, index_name: str = None):
"""Creates an index with given fields if not already created.
Index name will be `fieldname1_fieldname2_index`"""
index_name = index_name or self.get_index_name(fields)
- table_name = 'tab' + doctype
+ table_name = get_table_name(doctype)
if not self.has_index(table_name, index_name):
self.commit()
self.sql("""ALTER TABLE `%s`
diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql
index a52efd01e3..73b98f0ff3 100644
--- a/frappe/database/mariadb/framework_mariadb.sql
+++ b/frappe/database/mariadb/framework_mariadb.sql
@@ -61,6 +61,7 @@ CREATE TABLE `tabDocField` (
`in_preview` int(1) NOT NULL DEFAULT 0,
`read_only` int(1) 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,
@@ -71,7 +72,7 @@ CREATE TABLE `tabDocField` (
KEY `label` (`label`),
KEY `fieldtype` (`fieldtype`),
KEY `fieldname` (`fieldname`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
@@ -108,7 +109,7 @@ CREATE TABLE `tabDocPerm` (
`email` int(1) NOT NULL DEFAULT 1,
PRIMARY KEY (`name`),
KEY `parent` (`parent`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Table structure for table `tabDocType Action`
@@ -132,7 +133,7 @@ CREATE TABLE `tabDocType Action` (
PRIMARY KEY (`name`),
KEY `parent` (`parent`),
KEY `modified` (`modified`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
--
-- Table structure for table `tabDocType Action`
@@ -155,7 +156,7 @@ CREATE TABLE `tabDocType Link` (
PRIMARY KEY (`name`),
KEY `parent` (`parent`),
KEY `modified` (`modified`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
--
-- Table structure for table `tabDocType`
@@ -183,6 +184,7 @@ CREATE TABLE `tabDocType` (
`restrict_to_domain` varchar(255) DEFAULT NULL,
`app` varchar(255) DEFAULT NULL,
`autoname` varchar(255) DEFAULT NULL,
+ `naming_rule` varchar(40) DEFAULT NULL,
`name_case` varchar(255) DEFAULT NULL,
`title_field` varchar(255) DEFAULT NULL,
`image_field` varchar(255) DEFAULT NULL,
@@ -220,12 +222,14 @@ CREATE TABLE `tabDocType` (
`allow_guest_to_view` int(1) 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,
`subject_field` varchar(255) DEFAULT NULL,
`sender_field` varchar(255) DEFAULT NULL,
+ `migration_hash` varchar(255) DEFAULT NULL,
PRIMARY KEY (`name`),
KEY `parent` (`parent`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Table structure for table `tabSeries`
@@ -236,7 +240,7 @@ CREATE TABLE `tabSeries` (
`name` varchar(100),
`current` int(10) NOT NULL DEFAULT 0,
PRIMARY KEY(`name`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
@@ -253,7 +257,7 @@ CREATE TABLE `tabSessions` (
`device` varchar(255) DEFAULT 'desktop',
`status` varchar(20) DEFAULT NULL,
KEY `sid` (`sid`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
@@ -266,7 +270,7 @@ CREATE TABLE `tabSingles` (
`field` varchar(255) DEFAULT NULL,
`value` text,
KEY `singles_doctype_field_index` (`doctype`, `field`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Table structure for table `__Auth`
@@ -280,7 +284,7 @@ CREATE TABLE `__Auth` (
`password` TEXT NOT NULL,
`encrypted` INT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`doctype`, `name`, `fieldname`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Table structure for table `tabFile`
@@ -308,7 +312,7 @@ CREATE TABLE `tabFile` (
KEY `parent` (`parent`),
KEY `attached_to_name` (`attached_to_name`),
KEY `attached_to_doctype` (`attached_to_doctype`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Table structure for table `tabDefaultValue`
@@ -331,4 +335,4 @@ CREATE TABLE `tabDefaultValue` (
PRIMARY KEY (`name`),
KEY `parent` (`parent`),
KEY `defaultvalue_parent_defkey_index` (`parent`,`defkey`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py
index b40af59286..5768a2f23d 100644
--- a/frappe/database/mariadb/schema.py
+++ b/frappe/database/mariadb/schema.py
@@ -4,18 +4,22 @@ from frappe.database.schema import DBTable
class MariaDBTable(DBTable):
def create(self):
- add_text = ''
+ additional_definitions = ""
+ engine = self.meta.get("engine") or "InnoDB"
+ varchar_len = frappe.db.VARCHAR_LEN
# columns
column_defs = self.get_column_definitions()
- if column_defs: add_text += ',\n'.join(column_defs) + ',\n'
+ if column_defs:
+ additional_definitions += ',\n'.join(column_defs) + ',\n'
# index
index_defs = self.get_index_definitions()
- if index_defs: add_text += ',\n'.join(index_defs) + ',\n'
+ if index_defs:
+ additional_definitions += ',\n'.join(index_defs) + ',\n'
# create table
- frappe.db.sql("""create table `%s` (
+ query = f"""create table `{self.table_name}` (
name varchar({varchar_len}) not null primary key,
creation datetime(6),
modified datetime(6),
@@ -26,13 +30,15 @@ class MariaDBTable(DBTable):
parentfield varchar({varchar_len}),
parenttype varchar({varchar_len}),
idx int(8) not null default '0',
- %sindex parent(parent),
+ {additional_definitions}
+ index parent(parent),
index modified(modified))
ENGINE={engine}
- ROW_FORMAT=COMPRESSED
+ ROW_FORMAT=DYNAMIC
CHARACTER SET=utf8mb4
- COLLATE=utf8mb4_unicode_ci""".format(varchar_len=frappe.db.VARCHAR_LEN,
- engine=self.meta.get("engine") or 'InnoDB') % (self.table_name, add_text))
+ COLLATE=utf8mb4_unicode_ci"""
+
+ frappe.db.sql(query)
def alter(self):
for col in self.columns.values():
diff --git a/frappe/database/mariadb/setup_db.py b/frappe/database/mariadb/setup_db.py
index 6be08c66bb..8088cc2331 100644
--- a/frappe/database/mariadb/setup_db.py
+++ b/frappe/database/mariadb/setup_db.py
@@ -34,25 +34,23 @@ def setup_database(force, source_sql, verbose, no_mariadb_socket=False):
db_name = frappe.local.conf.db_name
root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password)
dbman = DbManager(root_conn)
+ dbman_kwargs = {}
+ if no_mariadb_socket:
+ dbman_kwargs["host"] = "%"
+
if force or (db_name not in dbman.get_database_list()):
- dbman.delete_user(db_name)
- if no_mariadb_socket:
- dbman.delete_user(db_name, host="%")
+ dbman.delete_user(db_name, **dbman_kwargs)
dbman.drop_database(db_name)
else:
raise Exception("Database %s already exists" % (db_name,))
- dbman.create_user(db_name, frappe.conf.db_password)
- if no_mariadb_socket:
- dbman.create_user(db_name, frappe.conf.db_password, host="%")
+ dbman.create_user(db_name, frappe.conf.db_password, **dbman_kwargs)
if verbose: print("Created user %s" % db_name)
dbman.create_database(db_name)
if verbose: print("Created database %s" % db_name)
- dbman.grant_all_privileges(db_name, db_name)
- if no_mariadb_socket:
- dbman.grant_all_privileges(db_name, db_name, host="%")
+ dbman.grant_all_privileges(db_name, db_name, **dbman_kwargs)
dbman.flush_privileges()
if verbose: print("Granted privileges to user %s and database %s" % (db_name, db_name))
diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py
index 8235277e30..bfa5515111 100644
--- a/frappe/database/postgres/database.py
+++ b/frappe/database/postgres/database.py
@@ -1,12 +1,15 @@
import re
-import frappe
+from typing import List, Tuple, Union
+
import psycopg2
import psycopg2.extensions
-from frappe.utils import cstr
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
+from psycopg2.errorcodes import STRING_DATA_RIGHT_TRUNCATION
+import frappe
from frappe.database.database import Database
from frappe.database.postgres.schema import PostgresTable
+from frappe.utils import cstr, get_table_name
# cast decimals as floats
DEC2FLOAT = psycopg2.extensions.new_type(
@@ -29,11 +32,11 @@ class PostgresDatabase(Database):
def setup_type_map(self):
self.db_type = 'postgres'
self.type_map = {
- 'Currency': ('decimal', '18,6'),
+ 'Currency': ('decimal', '21,9'),
'Int': ('bigint', None),
'Long Int': ('bigint', None),
- 'Float': ('decimal', '18,6'),
- 'Percent': ('decimal', '18,6'),
+ 'Float': ('decimal', '21,9'),
+ 'Percent': ('decimal', '21,9'),
'Check': ('smallint', None),
'Small Text': ('text', ''),
'Long Text': ('text', ''),
@@ -58,7 +61,8 @@ class PostgresDatabase(Database):
'Color': ('varchar', self.VARCHAR_LEN),
'Barcode': ('text', ''),
'Geolocation': ('text', ''),
- 'Duration': ('decimal', '18,6')
+ 'Duration': ('decimal', '21,9'),
+ 'Icon': ('varchar', self.VARCHAR_LEN)
}
def get_connection(self):
@@ -168,7 +172,20 @@ class PostgresDatabase(Database):
@staticmethod
def is_data_too_long(e):
- return e.pgcode == '22001'
+ return e.pgcode == STRING_DATA_RIGHT_TRUNCATION
+
+ def rename_table(self, old_name: str, new_name: str) -> Union[List, Tuple]:
+ old_name = get_table_name(old_name)
+ new_name = get_table_name(new_name)
+ return self.sql(f"ALTER TABLE `{old_name}` RENAME TO `{new_name}`")
+
+ def describe(self, doctype: str)-> Union[List, Tuple]:
+ table_name = get_table_name(doctype)
+ return self.sql(f"SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME = '{table_name}'")
+
+ def change_column_type(self, doctype: str, column: str, type: str) -> Union[List, Tuple]:
+ table_name = get_table_name(doctype)
+ return self.sql(f'ALTER TABLE "{table_name}" ALTER COLUMN "{column}" TYPE {type}')
def create_auth_table(self):
self.sql_ddl("""create table if not exists "__Auth" (
@@ -242,14 +259,14 @@ class PostgresDatabase(Database):
return self.sql("""SELECT 1 FROM pg_indexes WHERE tablename='{table_name}'
and indexname='{index_name}' limit 1""".format(table_name=table_name, index_name=index_name))
- def add_index(self, doctype, fields, index_name=None):
+ def add_index(self, doctype: str, fields: List, index_name: str = None):
"""Creates an index with given fields if not already created.
Index name will be `fieldname1_fieldname2_index`"""
+ table_name = get_table_name(doctype)
index_name = index_name or self.get_index_name(fields)
- table_name = 'tab' + doctype
+ fields_str = '", "'.join(re.sub(r"\(.*\)", "", field) for field in fields)
- self.commit()
- self.sql("""CREATE INDEX IF NOT EXISTS "{}" ON `{}`("{}")""".format(index_name, table_name, '", "'.join(fields)))
+ self.sql_ddl(f'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}` ("{fields_str}")')
def add_unique(self, doctype, fields, constraint_name=None):
if isinstance(fields, str):
@@ -297,6 +314,7 @@ class PostgresDatabase(Database):
def modify_query(query):
""""Modifies query according to the requirements of postgres"""
# replace ` with " for definitions
+ query = str(query)
query = query.replace('`', '"')
query = replace_locate_with_strpos(query)
# select from requires ""
diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql
index eeb0eecd3f..e8e047f194 100644
--- a/frappe/database/postgres/framework_postgres.sql
+++ b/frappe/database/postgres/framework_postgres.sql
@@ -61,6 +61,7 @@ CREATE TABLE "tabDocField" (
"in_preview" smallint NOT NULL DEFAULT 0,
"read_only" smallint NOT NULL DEFAULT 0,
"precision" varchar(255) DEFAULT NULL,
+ "max_height" varchar(10) DEFAULT NULL,
"length" bigint NOT NULL DEFAULT 0,
"translatable" smallint NOT NULL DEFAULT 0,
"hide_border" smallint NOT NULL DEFAULT 0,
@@ -188,6 +189,7 @@ CREATE TABLE "tabDocType" (
"restrict_to_domain" varchar(255) DEFAULT NULL,
"app" varchar(255) DEFAULT NULL,
"autoname" varchar(255) DEFAULT NULL,
+ "naming_rule" varchar(40) DEFAULT NULL,
"name_case" varchar(255) DEFAULT NULL,
"title_field" varchar(255) DEFAULT NULL,
"image_field" varchar(255) DEFAULT NULL,
@@ -225,9 +227,11 @@ CREATE TABLE "tabDocType" (
"allow_guest_to_view" smallint 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" smallint NOT NULL DEFAULT 0,
"subject_field" varchar(255) DEFAULT NULL,
"sender_field" varchar(255) DEFAULT NULL,
+ "migration_hash" varchar(255) DEFAULT NULL,
PRIMARY KEY ("name")
) ;
diff --git a/frappe/database/query.py b/frappe/database/query.py
new file mode 100644
index 0000000000..3545efb412
--- /dev/null
+++ b/frappe/database/query.py
@@ -0,0 +1,267 @@
+import operator
+from typing import Any, Dict, List, Tuple, Union
+
+import frappe
+from frappe.query_builder import Criterion, Order, Field
+
+
+def like(key: str, value: str) -> frappe.qb:
+ """Wrapper method for `LIKE`
+
+ Args:
+ key (str): field
+ value (str): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `LIKE`
+ """
+ return Field(key).like(value)
+
+
+def func_in(key: str, value: Union[List, Tuple]) -> frappe.qb:
+ """Wrapper method for `IN`
+
+ Args:
+ key (str): field
+ value (Union[int, str]): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `IN`
+ """
+ return Field(key).isin(value)
+
+
+def not_like(key: str, value: str) -> frappe.qb:
+ """Wrapper method for `NOT LIKE`
+
+ Args:
+ key (str): field
+ value (str): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `NOT LIKE`
+ """
+ return Field(key).not_like(value)
+
+
+def func_not_in(key: str, value: Union[List, Tuple]):
+ """Wrapper method for `NOT IN`
+
+ Args:
+ key (str): field
+ value (Union[int, str]): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `NOT IN`
+ """
+ return Field(key).notin(value)
+
+
+def func_regex(key: str, value: str) -> frappe.qb:
+ """Wrapper method for `REGEX`
+
+ Args:
+ key (str): field
+ value (str): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `REGEX`
+ """
+ return Field(key).regex(value)
+
+
+def func_between(key: str, value: Union[List, Tuple]) -> frappe.qb:
+ """Wrapper method for `BETWEEN`
+
+ Args:
+ key (str): field
+ value (Union[int, str]): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `BETWEEN`
+ """
+ return Field(key)[slice(*value)]
+
+def make_function(key: Any, value: Union[int, str]):
+ """returns fucntion query
+
+ Args:
+ key (Any): field
+ value (Union[int, str]): criterion
+
+ Returns:
+ frappe.qb: frappe.qb object
+ """
+ return OPERATOR_MAP[value[0]](key, value[1])
+
+
+def change_orderby(order: str):
+ """Convert orderby to standart Order object
+
+ Args:
+ order (str): Field, order
+
+ Returns:
+ tuple: field, order
+ """
+ order = order.split()
+ if order[1].lower() == "asc":
+ orderby, order = order[0], Order.asc
+ return orderby, order
+ orderby, order = order[0], Order.desc
+ return orderby, order
+
+
+OPERATOR_MAP = {
+ "+": operator.add,
+ "=": operator.eq,
+ "-": operator.sub,
+ "!=": operator.ne,
+ "<": operator.lt,
+ ">": operator.gt,
+ "<=": operator.le,
+ ">=": operator.ge,
+ "in": func_in,
+ "not in": func_not_in,
+ "like": like,
+ "not like": not_like,
+ "regex": func_regex,
+ "between": func_between
+ }
+
+
+class Query:
+ def get_condition(self, table: str, **kwargs) -> frappe.qb:
+ """Get initial table object
+
+ Args:
+ table (str): DocType
+
+ Returns:
+ frappe.qb: DocType with initial condition
+ """
+ if kwargs.get("update"):
+ return frappe.qb.update(table)
+ if kwargs.get("into"):
+ return frappe.qb.into(table)
+ return frappe.qb.from_(table)
+
+ def criterion_query(self, table: str, criterion: Criterion, **kwargs) -> frappe.qb:
+ """Generate filters from Criterion objects
+
+ Args:
+ table (str): DocType
+ criterion (Criterion): Filters
+
+ Returns:
+ frappe.qb: condition object
+ """
+ condition = self.add_conditions(self.get_condition(table, **kwargs), **kwargs)
+ return condition.where(criterion)
+
+ def add_conditions(self, conditions: frappe.qb, **kwargs):
+ """Adding additional conditions
+
+ Args:
+ conditions (frappe.qb): built conditions
+
+ Returns:
+ conditions (frappe.qb): frappe.qb object
+ """
+ if kwargs.get("orderby"):
+ orderby = kwargs.get("orderby")
+ order = kwargs.get("order") if kwargs.get("order") else Order.desc
+ if isinstance(orderby, str) and len(orderby.split()) > 1:
+ orderby, order = change_orderby(orderby)
+ conditions = conditions.orderby(orderby, order=order)
+
+ if kwargs.get("limit"):
+ conditions = conditions.limit(kwargs.get("limit"))
+
+ if kwargs.get("distinct"):
+ conditions = conditions.distinct()
+
+ if kwargs.get("for_update"):
+ conditions = conditions.for_update()
+
+ return conditions
+
+ def misc_query(self, table: str, filters: Union[List, Tuple] = None, **kwargs):
+ """Build conditions using the given Lists or Tuple filters
+
+ Args:
+ table (str): DocType
+ filters (Union[List, Tuple], optional): Filters. Defaults to None.
+ """
+ conditions = self.get_condition(table, **kwargs)
+ if not filters:
+ return conditions
+ if isinstance(filters, list):
+ for f in filters:
+ if not isinstance(f, (list, tuple)):
+ _operator = OPERATOR_MAP[filters[1]]
+ if not isinstance(filters[0], str):
+ conditions = make_function(filters[0], filters[2])
+ break
+ conditions = conditions.where(_operator(Field(filters[0]), filters[2]))
+ break
+ else:
+ _operator = OPERATOR_MAP[f[1]]
+ conditions = conditions.where(_operator(Field(f[0]), f[2]))
+
+ conditions = self.add_conditions(conditions, **kwargs)
+ return conditions
+
+ def dict_query(self, table: str, filters: Dict[str, Union[str, int]] = None, **kwargs) -> frappe.qb:
+ """Build conditions using the given dictionary filters
+
+ Args:
+ table (str): DocType
+ filters (Dict[str, Union[str, int]], optional): Filters. Defaults to None.
+
+ Returns:
+ frappe.qb: conditions object
+ """
+ conditions = self.get_condition(table, **kwargs)
+ if not filters:
+ return conditions
+
+ for key in filters:
+ value = filters.get(key)
+ _operator = OPERATOR_MAP["="]
+
+ if not isinstance(key, str):
+ conditions = conditions.where(make_function(key, value))
+ continue
+ if isinstance(value, (list, tuple)):
+ if isinstance(value[1], (list, tuple)) or value[0] in list(OPERATOR_MAP.keys())[-4:]:
+ _operator = OPERATOR_MAP[value[0]]
+ conditions = conditions.where(_operator(key, value[1]))
+ else:
+ _operator = OPERATOR_MAP[value[0]]
+ conditions = conditions.where(_operator(Field(key), value[1]))
+ else:
+ conditions = conditions.where(_operator(Field(key), value))
+ conditions = self.add_conditions(conditions, **kwargs)
+ return conditions
+
+ def build_conditions(self, table: str, filters: Union[Dict[str, Union[str, int]], str, int] = None, **kwargs) -> frappe.qb:
+ """Build conditions for sql query
+
+ Args:
+ filters (Union[Dict[str, Union[str, int]], str, int]): conditions in Dict
+ table (str): DocType
+
+ Returns:
+ frappe.qb: frappe.qb conditions object
+ """
+ if isinstance(filters, Criterion):
+ return self.criterion_query(table, filters, **kwargs)
+
+ if isinstance(filters, int) or isinstance(filters, str):
+ filters = {"name": str(filters)}
+
+ if isinstance(filters, (list, tuple)):
+ return self.misc_query(table, filters, **kwargs)
+
+ return self.dict_query(filters=filters, table=table, **kwargs)
diff --git a/frappe/database/schema.py b/frappe/database/schema.py
index 31f11dbd5e..ce9fcb4147 100644
--- a/frappe/database/schema.py
+++ b/frappe/database/schema.py
@@ -303,6 +303,8 @@ def get_definition(fieldtype, precision=None, length=None):
size = d[1] if d[1] else None
if size:
+ # This check needs to exist for backward compatibility.
+ # Till V13, default size used for float, currency and percent are (18, 6).
if fieldtype in ["Float", "Currency", "Percent"] and cint(precision) > 6:
size = '21,9'
diff --git a/frappe/defaults.py b/frappe/defaults.py
index fde48d71ff..eb98db449f 100644
--- a/frappe/defaults.py
+++ b/frappe/defaults.py
@@ -1,9 +1,10 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.desk.notifications import clear_notifications
from frappe.cache_manager import clear_defaults_cache, common_default_keys
+from frappe.query_builder import DocType
# Note: DefaultValue records are identified by parenttype
# __default, __global or 'User Permission'
@@ -116,19 +117,15 @@ def set_default(key, value, parent, parenttype="__default"):
:param value: Default value.
:param parent: Usually, **User** to whom the default belongs.
:param parenttype: [optional] default is `__default`."""
- if frappe.db.sql('''
- select
- defkey
- from
- `tabDefaultValue`
- where
- defkey=%s and parent=%s
- for update''', (key, parent)):
- frappe.db.sql("""
- delete from
- `tabDefaultValue`
- where
- defkey=%s and parent=%s""", (key, parent))
+ table = DocType("DefaultValue")
+ key_exists = frappe.qb.from_(table).where(
+ (table.defkey == key) & (table.parent == parent)
+ ).select(table.defkey).for_update().run()
+ if key_exists:
+ frappe.db.delete("DefaultValue", {
+ "defkey": key,
+ "parent": parent
+ })
if value != None:
add_default(key, value, parent)
else:
@@ -155,29 +152,23 @@ def clear_default(key=None, value=None, parent=None, name=None, parenttype=None)
:param name: Default ID.
:param parenttype: Clear defaults table for a particular type e.g. **User**.
"""
- conditions = []
- values = []
+ filters = {}
if name:
- conditions.append("name=%s")
- values.append(name)
+ filters.update({"name": name})
else:
if key:
- conditions.append("defkey=%s")
- values.append(key)
+ filters.update({"defkey": key})
if value:
- conditions.append("defvalue=%s")
- values.append(value)
+ filters.update({"defvalue": value})
if parent:
- conditions.append("parent=%s")
- values.append(parent)
+ filters.update({"parent": parent})
if parenttype:
- conditions.append("parenttype=%s")
- values.append(parenttype)
+ filters.update({"parenttype": parenttype})
if parent:
clear_defaults_cache(parent)
@@ -185,11 +176,10 @@ def clear_default(key=None, value=None, parent=None, name=None, parenttype=None)
clear_defaults_cache("__default")
clear_defaults_cache("__global")
- if not conditions:
+ if not filters:
raise Exception("[clear_default] No key specified.")
- frappe.db.sql("""delete from tabDefaultValue where {0}""".format(" and ".join(conditions)),
- tuple(values))
+ frappe.db.delete("DefaultValue", filters)
_clear_cache(parent)
@@ -199,8 +189,12 @@ def get_defaults_for(parent="__default"):
if defaults==None:
# sort descending because first default must get precedence
- res = frappe.db.sql("""select defkey, defvalue from `tabDefaultValue`
- where parent = %s order by creation""", (parent,), as_dict=1)
+ table = DocType("DefaultValue")
+ res = frappe.qb.from_(table).where(
+ table.parent == parent
+ ).select(
+ table.defkey, table.defvalue
+ ).orderby("creation").run(as_dict=True)
defaults = frappe._dict({})
for d in res:
diff --git a/frappe/desk/__init__.py b/frappe/desk/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/desk/__init__.py
+++ b/frappe/desk/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/desk/calendar.py b/frappe/desk/calendar.py
index f00f729415..66e6dd8434 100644
--- a/frappe/desk/calendar.py
+++ b/frappe/desk/calendar.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
@@ -25,7 +25,6 @@ def get_event_conditions(doctype, filters=None):
@frappe.whitelist()
def get_events(doctype, start, end, field_map, filters=None, fields=None):
-
field_map = frappe._dict(json.loads(field_map))
fields = frappe.parse_json(fields)
@@ -36,8 +35,7 @@ def get_events(doctype, start, end, field_map, filters=None, fields=None):
"color": d.fieldname
})
- if filters:
- filters = json.loads(filters or '')
+ filters = json.loads(filters) if filters else []
if not fields:
fields = [field_map.start, field_map.end, field_map.title, 'name']
@@ -52,5 +50,5 @@ def get_events(doctype, start, end, field_map, filters=None, fields=None):
[doctype, start_date, '<=', end],
[doctype, end_date, '>=', start],
]
-
+ fields = list({field for field in fields if field})
return frappe.get_list(doctype, fields=fields, filters=filters)
diff --git a/frappe/desk/desk_page.py b/frappe/desk/desk_page.py
index d373dbda0e..a01008280c 100644
--- a/frappe/desk/desk_page.py
+++ b/frappe/desk/desk_page.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.translate import send_translations
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index 0a7d436169..e1789852f1 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -1,11 +1,12 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
# Author - Shivam Mishra
import frappe
from json import loads, dumps
from frappe import _, DoesNotExistError, ValidationError, _dict
from frappe.boot import get_allowed_pages, get_allowed_reports
+from frappe.core.doctype.custom_role.custom_role import get_custom_allowed_roles
from functools import wraps
from frappe.cache_manager import (
build_domain_restriced_doctype_cache,
@@ -27,18 +28,18 @@ def handle_not_exist(fn):
class Workspace:
- def __init__(self, page_name, minimal=False):
- self.page_name = page_name
- self.extended_links = []
- self.extended_charts = []
- self.extended_shortcuts = []
+ def __init__(self, page, minimal=False):
+ self.page_name = page.get('name')
+ self.page_title = page.get('title')
+ self.public_page = page.get('public')
+ self.workspace_manager = "Workspace Manager" in frappe.get_roles()
self.user = frappe.get_user()
self.allowed_modules = self.get_cached('user_allowed_modules', self.get_allowed_modules)
- self.doc = self.get_page_for_user()
+ self.doc = frappe.get_cached_doc("Workspace", self.page_name)
- if self.doc.module and self.doc.module not in self.allowed_modules:
+ if self.doc and self.doc.module and self.doc.module not in self.allowed_modules and not self.workspace_manager:
raise frappe.PermissionError
self.can_read = self.get_cached('user_perm_can_read', self.get_can_read_items)
@@ -47,16 +48,17 @@ class Workspace:
self.allowed_reports = get_allowed_reports(cache=True)
if not minimal:
- self.onboarding_doc = self.get_onboarding_doc()
- self.onboarding = None
+ if self.doc.content:
+ self.onboarding_list = [x['data']['onboarding_name'] for x in loads(self.doc.content) if x['type'] == 'onboarding']
+ self.onboardings = []
self.table_counts = get_table_with_counts()
self.restricted_doctypes = frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache()
self.restricted_pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache()
def is_page_allowed(self):
- cards = self.doc.get_link_groups() + get_custom_reports_and_doctypes(self.doc.module) + self.extended_links
- shortcuts = self.doc.shortcuts + self.extended_shortcuts
+ cards = self.doc.get_link_groups() + get_custom_reports_and_doctypes(self.doc.module)
+ shortcuts = self.doc.shortcuts
for section in cards:
links = loads(section.get('links')) if isinstance(section.get('links'), str) else section.get('links')
@@ -74,8 +76,28 @@ class Workspace:
if self.is_item_allowed(item.link_to, item.type) and _in_active_domains(item):
return True
+ if not shortcuts and not self.doc.links:
+ return True
+
return False
+ def is_permitted(self):
+ """Returns true if Has Role is not set or the user is allowed."""
+ from frappe.utils import has_common
+
+ allowed = [d.role for d in frappe.get_all("Has Role", fields=["role"], filters={"parent": self.doc.name})]
+
+ custom_roles = get_custom_allowed_roles('page', self.doc.name)
+ allowed.extend(custom_roles)
+
+ if not allowed:
+ return True
+
+ roles = frappe.get_roles()
+
+ if has_common(roles, allowed):
+ return True
+
def get_cached(self, cache_key, fallback_fn):
_cache = frappe.cache()
@@ -101,39 +123,18 @@ class Workspace:
return self.user.allow_modules
- def get_page_for_user(self):
- filters = {
- 'extends': self.page_name,
- 'for_user': frappe.session.user
- }
- user_pages = frappe.get_all("Workspace", filters=filters, limit=1)
- if user_pages:
- return frappe.get_cached_doc("Workspace", user_pages[0])
-
- filters = {
- 'extends_another_page': 1,
- 'extends': self.page_name,
- 'is_default': 1
- }
- default_page = frappe.get_all("Workspace", filters=filters, limit=1)
- if default_page:
- return frappe.get_cached_doc("Workspace", default_page[0])
-
- self.get_pages_to_extend()
- return frappe.get_cached_doc("Workspace", self.page_name)
-
- def get_onboarding_doc(self):
+ def get_onboarding_doc(self, onboarding):
# Check if onboarding is enabled
if not frappe.get_system_settings("enable_onboarding"):
return None
- if not self.doc.onboarding:
+ if not self.onboarding_list:
return None
- if frappe.db.get_value("Module Onboarding", self.doc.onboarding, "is_complete"):
+ if frappe.db.get_value("Module Onboarding", onboarding, "is_complete"):
return None
- doc = frappe.get_doc("Module Onboarding", self.doc.onboarding)
+ doc = frappe.get_doc("Module Onboarding", onboarding)
# Check if user is allowed
allowed_roles = set(doc.get_allowed_roles())
@@ -147,21 +148,6 @@ class Workspace:
return doc
- def get_pages_to_extend(self):
- pages = frappe.get_all("Workspace", filters={
- "extends": self.page_name,
- 'restrict_to_domain': ['in', frappe.get_active_domains()],
- 'for_user': '',
- 'module': ['in', self.allowed_modules]
- })
-
- pages = [frappe.get_cached_doc("Workspace", page['name']) for page in pages]
-
- for page in pages:
- self.extended_links = self.extended_links + page.get_link_groups()
- self.extended_charts = self.extended_charts + page.charts
- self.extended_shortcuts = self.extended_shortcuts + page.shortcuts
-
def is_item_allowed(self, name, item_type):
if frappe.session.user == "Administrator":
return True
@@ -183,28 +169,20 @@ class Workspace:
def build_workspace(self):
self.cards = {
- 'label': _(self.doc.cards_label),
'items': self.get_links()
}
self.charts = {
- 'label': _(self.doc.charts_label),
'items': self.get_charts()
}
self.shortcuts = {
- 'label': _(self.doc.shortcuts_label),
'items': self.get_shortcuts()
}
- if self.onboarding_doc:
- self.onboarding = {
- 'label': _(self.onboarding_doc.title),
- 'subtitle': _(self.onboarding_doc.subtitle),
- 'success': _(self.onboarding_doc.success_message),
- 'docs_url': self.onboarding_doc.documentation_url,
- 'items': self.get_onboarding_steps()
- }
+ self.onboardings = {
+ 'items': self.get_onboardings()
+ }
def _doctype_contains_a_record(self, name):
exists = self.table_counts.get(name, False)
@@ -250,9 +228,6 @@ class Workspace:
if not self.doc.hide_custom:
cards = cards + get_custom_reports_and_doctypes(self.doc.module)
- if len(self.extended_links):
- cards = merge_cards_based_on_label(cards + self.extended_links)
-
default_country = frappe.db.get_default("country")
new_data = []
@@ -290,8 +265,6 @@ class Workspace:
all_charts = []
if frappe.has_permission("Dashboard Chart", throw=False):
charts = self.doc.charts
- if len(self.extended_charts):
- charts = charts + self.extended_charts
for chart in charts:
if frappe.has_permission('Dashboard Chart', doc=chart.chart_name):
@@ -312,8 +285,6 @@ class Workspace:
items = []
shortcuts = self.doc.shortcuts
- if len(self.extended_shortcuts):
- shortcuts = shortcuts + self.extended_shortcuts
for item in shortcuts:
new_item = item.as_dict().copy()
@@ -333,9 +304,26 @@ class Workspace:
return items
@handle_not_exist
- def get_onboarding_steps(self):
+ def get_onboardings(self):
+ if self.onboarding_list:
+ for onboarding in self.onboarding_list:
+ onboarding_doc = self.get_onboarding_doc(onboarding)
+ if onboarding_doc:
+ item = {
+ 'label': _(onboarding),
+ 'title': _(onboarding_doc.title),
+ 'subtitle': _(onboarding_doc.subtitle),
+ 'success': _(onboarding_doc.success_message),
+ 'docs_url': onboarding_doc.documentation_url,
+ 'items': self.get_onboarding_steps(onboarding_doc)
+ }
+ self.onboardings.append(item)
+ return self.onboardings
+
+ @handle_not_exist
+ def get_onboarding_steps(self, onboarding_doc):
steps = []
- for doc in self.onboarding_doc.get_steps():
+ for doc in onboarding_doc.get_steps():
step = doc.as_dict().copy()
step.label = _(doc.title)
if step.action == "Create Entry":
@@ -352,58 +340,64 @@ def get_desktop_page(page):
on desk.
Args:
- page (string): page name
+ page (json): page data
Returns:
dict: dictionary of cards, charts and shortcuts to be displayed on website
"""
try:
- wspace = Workspace(page)
+ wspace = Workspace(loads(page))
wspace.build_workspace()
return {
'charts': wspace.charts,
'shortcuts': wspace.shortcuts,
'cards': wspace.cards,
- 'onboarding': wspace.onboarding,
- 'allow_customization': not wspace.doc.disable_user_customization
+ 'onboardings': wspace.onboardings
}
except DoesNotExistError:
+ frappe.log_error(frappe.get_traceback())
return {}
@frappe.whitelist()
-def get_desk_sidebar_items():
+def get_wspace_sidebar_items():
"""Get list of sidebar items for desk"""
+ has_access = "Workspace Manager" in frappe.get_roles()
# don't get domain restricted pages
blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules()
+ blocked_modules.append('Dummy Module')
filters = {
'restrict_to_domain': ['in', frappe.get_active_domains()],
- 'extends_another_page': 0,
- 'for_user': '',
'module': ['not in', blocked_modules]
}
- if not frappe.local.conf.developer_mode:
- filters['developer_mode_only'] = '0'
+ if has_access:
+ filters = []
- # pages sorted based on pinned to top and then by name
- order_by = "pin_to_top desc, pin_to_bottom asc, name asc"
- all_pages = frappe.get_all("Workspace", fields=["name", "category", "icon", "module"],
- filters=filters, order_by=order_by, ignore_permissions=True)
+ # pages sorted based on sequence id
+ order_by = "sequence_id asc"
+ fields = ["name", "title", "for_user", "parent_page", "content", "public", "module", "icon"]
+ all_pages = frappe.get_all("Workspace", fields=fields, filters=filters, order_by=order_by, ignore_permissions=True)
pages = []
+ private_pages = []
# Filter Page based on Permission
for page in all_pages:
try:
- wspace = Workspace(page.get('name'), True)
- if wspace.is_page_allowed():
- pages.append(page)
+ wspace = Workspace(page, True)
+ if wspace.is_permitted() and wspace.is_page_allowed() or has_access:
+ if page.public:
+ pages.append(page)
+ elif page.for_user == frappe.session.user:
+ private_pages.append(page)
page['label'] = _(page.get('name'))
except frappe.PermissionError:
pass
+ if private_pages:
+ pages.extend(private_pages)
- return pages
+ return {'pages': pages, 'has_access': has_access}
def get_table_with_counts():
counts = frappe.cache().get_value("information_schema:counts")
@@ -438,7 +432,6 @@ def get_custom_doctype_list(module):
return out
-
def get_custom_report_list(module):
"""Returns list on new style reports for modules."""
reports = frappe.get_all("Report", fields=["name", "ref_doctype", "report_type"], filters=
@@ -451,6 +444,7 @@ def get_custom_report_list(module):
"type": "Link",
"link_type": "report",
"doctype": r.ref_doctype,
+ "dependencies": r.ref_doctype,
"is_query_report": 1 if r.report_type in ("Query Report", "Script Report", "Custom Report") else 0,
"label": _(r.name),
"link_to": r.name,
@@ -458,73 +452,26 @@ def get_custom_report_list(module):
return out
-def get_custom_workspace_for_user(page):
- """Get custom page from workspace if exists or create one
+def save_new_widget(doc, page, blocks, new_widgets):
- Args:
- page (stirng): Page name
+ widgets = _dict(loads(new_widgets))
- Returns:
- Object: Document object
- """
- filters = {
- 'extends': page,
- 'for_user': frappe.session.user
- }
- pages = frappe.get_list("Workspace", filters=filters)
- if pages:
- return frappe.get_doc("Workspace", pages[0])
- doc = frappe.new_doc("Workspace")
- doc.extends = page
- doc.for_user = frappe.session.user
- return doc
+ if widgets.chart:
+ doc.charts.extend(new_widget(widgets.chart, "Workspace Chart", "charts"))
+ if widgets.shortcut:
+ doc.shortcuts.extend(new_widget(widgets.shortcut, "Workspace Shortcut", "shortcuts"))
+ if widgets.card:
+ doc.build_links_table_from_card(widgets.card)
-
-@frappe.whitelist()
-def save_customization(page, config):
- """Save customizations as a separate doctype in Workspace per user
-
- Args:
- page (string): Name of the page to be edited
- config (dict): Dictionary config of al widgets
-
- Returns:
- Boolean: Customization saving status
- """
- original_page = frappe.get_doc("Workspace", page)
- page_doc = get_custom_workspace_for_user(page)
-
- # Update field values
- page_doc.update({
- "icon": original_page.icon,
- "charts_label": original_page.charts_label,
- "cards_label": original_page.cards_label,
- "shortcuts_label": original_page.shortcuts_label,
- "module": original_page.module,
- "onboarding": original_page.onboarding,
- "developer_mode_only": original_page.developer_mode_only,
- "category": original_page.category
- })
-
- config = _dict(loads(config))
- if config.charts:
- page_doc.charts = prepare_widget(config.charts, "Workspace Chart", "charts")
- if config.shortcuts:
- page_doc.shortcuts = prepare_widget(config.shortcuts, "Workspace Shortcut", "shortcuts")
- if config.cards:
- page_doc.build_links_table_from_cards(config.cards)
-
- # Set label
- page_doc.label = page + '-' + frappe.session.user
+ # remove duplicate and unwanted widgets
+ if widgets:
+ clean_up(doc, blocks)
try:
- if page_doc.is_new():
- page_doc.insert(ignore_permissions=True)
- else:
- page_doc.save(ignore_permissions=True)
+ doc.save(ignore_permissions=True)
except (ValidationError, TypeError) as e:
# Create a json string to log
- json_config = dumps(config, sort_keys=True, indent=4)
+ json_config = dumps(widgets, sort_keys=True, indent=4)
# Error log body
log = \
@@ -538,6 +485,48 @@ def save_customization(page, config):
return True
+def clean_up(original_page, blocks):
+ page_widgets = {}
+
+ for wid in ['shortcut', 'card', 'chart']:
+ # get list of widget's name from blocks
+ page_widgets[wid] = [x['data'][wid + '_name'] for x in loads(blocks) if x['type'] == wid]
+
+ # shortcut & chart cleanup
+ for wid in ['shortcut', 'chart']:
+ updated_widgets = []
+ original_page.get(wid+'s').reverse()
+
+ for w in original_page.get(wid+'s'):
+ if w.label in page_widgets[wid] and w.label not in [x.label for x in updated_widgets]:
+ updated_widgets.append(w)
+ original_page.set(wid+'s', updated_widgets)
+
+ # card cleanup
+ for i, v in enumerate(original_page.links):
+ if v.type == 'Card Break' and v.label not in page_widgets['card']:
+ del original_page.links[i : i+v.link_count+1]
+
+def new_widget(config, doctype, parentfield):
+ if not config:
+ return []
+ prepare_widget_list = []
+ for idx, widget in enumerate(config):
+ # Some cleanup
+ widget.pop("name", None)
+
+ # New Doc
+ doc = frappe.new_doc(doctype)
+ doc.update(widget)
+
+ # Manually Set IDX
+ doc.idx = idx + 1
+
+ # Set Parent Field
+ doc.parentfield = parentfield
+
+ prepare_widget_list.append(doc)
+ return prepare_widget_list
def prepare_widget(config, doctype, parentfield):
"""Create widget child table entries with parent details
@@ -573,40 +562,14 @@ def prepare_widget(config, doctype, parentfield):
prepare_widget_list.append(doc)
return prepare_widget_list
-
@frappe.whitelist()
def update_onboarding_step(name, field, value):
"""Update status of onboaridng step
Args:
- name (string): Name of the doc
- field (string): field to be updated
- value: Value to be updated
+ name (string): Name of the doc
+ field (string): field to be updated
+ value: Value to be updated
"""
frappe.db.set_value("Onboarding Step", name, field, value)
-
-@frappe.whitelist()
-def reset_customization(page):
- """Reset workspace customizations for a user
-
- Args:
- page (string): Name of the page to be reset
- """
- page_doc = get_custom_workspace_for_user(page)
- page_doc.delete()
-
-def merge_cards_based_on_label(cards):
- """Merge cards with common label."""
- cards_dict = {}
- for card in cards:
- label = card.get('label')
- if label in cards_dict:
- links = cards_dict[label].links + card.links
- cards_dict[label].update(dict(links=links))
- cards_dict[label] = cards_dict.pop(label)
- else:
- cards_dict[label] = card
-
- return list(cards_dict.values())
-
diff --git a/frappe/desk/doctype/bulk_update/bulk_update.py b/frappe/desk/doctype/bulk_update/bulk_update.py
index 469ee839f1..b512ca175c 100644
--- a/frappe/desk/doctype/bulk_update/bulk_update.py
+++ b/frappe/desk/doctype/bulk_update/bulk_update.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/calendar_view/calendar_view.py b/frappe/desk/doctype/calendar_view/calendar_view.py
index 3a986f3273..11612f5587 100644
--- a/frappe/desk/doctype/calendar_view/calendar_view.py
+++ b/frappe/desk/doctype/calendar_view/calendar_view.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/console_log/console_log.py b/frappe/desk/doctype/console_log/console_log.py
index 5d0f1cfa93..e0b552ebfd 100644
--- a/frappe/desk/doctype/console_log/console_log.py
+++ b/frappe/desk/doctype/console_log/console_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/console_log/test_console_log.py b/frappe/desk/doctype/console_log/test_console_log.py
index 3bb1605204..c41b9d68c8 100644
--- a/frappe/desk/doctype/console_log/test_console_log.py
+++ b/frappe/desk/doctype/console_log/test_console_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py
index 1d333609db..0dfd458a37 100644
--- a/frappe/desk/doctype/dashboard/dashboard.py
+++ b/frappe/desk/doctype/dashboard/dashboard.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
diff --git a/frappe/desk/doctype/dashboard/test_dashboard.py b/frappe/desk/doctype/dashboard/test_dashboard.py
index dd1bc31d86..15c132c027 100644
--- a/frappe/desk/doctype/dashboard/test_dashboard.py
+++ b/frappe/desk/doctype/dashboard/test_dashboard.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import unittest
class TestDashboard(unittest.TestCase):
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
index 3b4d5e7be5..e0d2cab8ef 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
@@ -45,6 +45,7 @@ frappe.ui.form.on('Dashboard Chart', {
frm.set_df_property("filters_section", "hidden", 1);
frm.set_df_property("dynamic_filters_section", "hidden", 1);
+ frm.trigger('set_parent_document_type');
frm.trigger('set_time_series');
frm.set_query('document_type', function() {
return {
@@ -110,9 +111,11 @@ frappe.ui.form.on('Dashboard Chart', {
frm.set_value('source', '');
frm.set_value('based_on', '');
frm.set_value('value_based_on', '');
+ frm.set_value('parent_document_type', '');
frm.set_value('filters_json', '[]');
frm.set_value('dynamic_filters_json', '[]');
frm.trigger('update_options');
+ frm.trigger('set_parent_document_type');
},
report_name: function(frm) {
@@ -125,7 +128,6 @@ frappe.ui.form.on('Dashboard Chart', {
frm.trigger('set_chart_report_filters');
},
-
set_chart_report_filters: function(frm) {
let report_name = frm.doc.report_name;
@@ -148,6 +150,10 @@ frappe.ui.form.on('Dashboard Chart', {
}
},
+ use_report_chart: function(frm) {
+ !frm.doc.use_report_chart && frm.trigger('set_chart_field_options');
+ },
+
set_chart_field_options: function(frm) {
let filters = frm.doc.filters_json.length > 2 ? JSON.parse(frm.doc.filters_json) : null;
if (frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2) {
@@ -179,6 +185,9 @@ frappe.ui.form.on('Dashboard Chart', {
} else {
frappe.msgprint(__('Report has no data, please modify the filters or change the Report Name'));
}
+ } else {
+ frm.set_value('use_report_chart', 1);
+ frm.set_df_property('use_report_chart', 'hidden', false);
}
});
},
@@ -223,7 +232,7 @@ frappe.ui.form.on('Dashboard Chart', {
if (['Date', 'Datetime'].includes(df.fieldtype)) {
date_fields.push({label: df.label, value: df.fieldname});
}
- if (['Int', 'Float', 'Currency', 'Percent'].includes(df.fieldtype)) {
+ if (['Int', 'Float', 'Currency', 'Percent', 'Duration'].includes(df.fieldtype)) {
value_fields.push({label: df.label, value: df.fieldname});
aggregate_function_fields.push({label: df.label, value: df.fieldname});
}
@@ -365,6 +374,7 @@ frappe.ui.form.on('Dashboard Chart', {
frm.filter_group = new frappe.ui.FilterGroup({
parent: dialog.get_field('filter_area').$wrapper,
doctype: frm.doc.document_type,
+ parent_doctype: frm.doc.parent_document_type,
on_change: () => {},
});
@@ -481,6 +491,36 @@ frappe.ui.form.on('Dashboard Chart', {
frm.dynamic_filter_table.find('tbody').html(filter_rows);
}
+ },
+
+ set_parent_document_type: async function(frm) {
+ let document_type = frm.doc.document_type;
+ let doc_is_table = document_type &&
+ (await frappe.db.get_value('DocType', document_type, 'istable')).message.istable;
+
+ frm.set_df_property('parent_document_type', 'hidden', !doc_is_table);
+
+ if (document_type && doc_is_table) {
+ let parent = await frappe.db.get_list('DocField', {
+ filters: {
+ 'fieldtype': 'Table',
+ 'options': document_type
+ },
+ fields: ['parent']
+ });
+
+ parent && frm.set_query('parent_document_type', function() {
+ return {
+ filters: {
+ "name": ['in', parent.map(({ parent }) => parent)]
+ }
+ };
+ });
+
+ if (parent.length === 1) {
+ frm.set_value('parent_document_type', parent[0].parent);
+ }
+ }
}
});
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
index d4bba53068..a5d30c10e5 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
@@ -17,6 +17,7 @@
"y_axis",
"source",
"document_type",
+ "parent_document_type",
"based_on",
"value_based_on",
"group_by_type",
@@ -268,10 +269,18 @@
"fieldname": "use_report_chart",
"fieldtype": "Check",
"label": "Use Report Chart"
+ },
+ {
+ "depends_on": "eval: doc.chart_type !== 'Custom' && doc.chart_type !== 'Report'",
+ "description": "The document type selected is a child table, so the parent document type is required.",
+ "fieldname": "parent_document_type",
+ "fieldtype": "Link",
+ "label": "Parent Document Type",
+ "options": "DocType"
}
],
"links": [],
- "modified": "2020-07-23 11:10:33.509497",
+ "modified": "2021-11-09 17:18:11.456145",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
index db5964e7b2..cb77ef7a1a 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
@@ -333,7 +333,10 @@ class DashboardChart(Document):
def check_required_field(self):
if not self.document_type:
- frappe.throw(_("Document type is required to create a dashboard chart"))
+ frappe.throw(_("Document type is required to create a dashboard chart"))
+
+ if self.document_type and frappe.get_meta(self.document_type).istable and not self.parent_document_type:
+ frappe.throw(_("Parent document type is required to create a dashboard chart"))
if self.chart_type == 'Group By':
if not self.group_by_based_on:
diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
index 78d133b2d5..5562f2fc92 100644
--- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import unittest, frappe
from frappe.utils import getdate, formatdate, get_last_day
from frappe.utils.dateutils import get_period_ending, get_period
@@ -64,7 +64,7 @@ class TestDashboardChart(unittest.TestCase):
if frappe.db.exists('Dashboard Chart', 'Test Empty Dashboard Chart'):
frappe.delete_doc('Dashboard Chart', 'Test Empty Dashboard Chart')
- frappe.db.sql('delete from `tabError Log`')
+ frappe.db.delete("Error Log")
frappe.get_doc(dict(
doctype = 'Dashboard Chart',
@@ -94,7 +94,7 @@ class TestDashboardChart(unittest.TestCase):
if frappe.db.exists('Dashboard Chart', 'Test Empty Dashboard Chart 2'):
frappe.delete_doc('Dashboard Chart', 'Test Empty Dashboard Chart 2')
- frappe.db.sql('delete from `tabError Log`')
+ frappe.db.delete("Error Log")
# create one data point
frappe.get_doc(dict(doctype = 'Error Log', creation = '2018-06-01 00:00:00')).insert()
diff --git a/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.py b/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.py
index 7d6f66daa2..8b2fba2e58 100644
--- a/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.py
+++ b/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/dashboard_chart_link/dashboard_chart_link.py b/frappe/desk/doctype/dashboard_chart_link/dashboard_chart_link.py
index 359801a303..87d095d5d1 100644
--- a/frappe/desk/doctype/dashboard_chart_link/dashboard_chart_link.py
+++ b/frappe/desk/doctype/dashboard_chart_link/dashboard_chart_link.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.py b/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.py
index 791dbc563b..71ded32837 100644
--- a/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.py
+++ b/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe, os
from frappe import _
diff --git a/frappe/desk/doctype/dashboard_chart_source/test_dashboard_chart_source.py b/frappe/desk/doctype/dashboard_chart_source/test_dashboard_chart_source.py
index 53fe127dfb..6d6773d52e 100644
--- a/frappe/desk/doctype/dashboard_chart_source/test_dashboard_chart_source.py
+++ b/frappe/desk/doctype/dashboard_chart_source/test_dashboard_chart_source.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import unittest
class TestDashboardChartSource(unittest.TestCase):
diff --git a/frappe/desk/doctype/dashboard_settings/dashboard_settings.py b/frappe/desk/doctype/dashboard_settings/dashboard_settings.py
index df61c52114..2f29b3e989 100644
--- a/frappe/desk/doctype/dashboard_settings/dashboard_settings.py
+++ b/frappe/desk/doctype/dashboard_settings/dashboard_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.py b/frappe/desk/doctype/desktop_icon/desktop_icon.py
index 81a79cdb09..194b0d0ca4 100644
--- a/frappe/desk/doctype/desktop_icon/desktop_icon.py
+++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
@@ -197,7 +197,7 @@ def set_desktop_icons(visible_list, ignore_duplicate=True):
# clear all custom only if setup is not complete
if not int(frappe.defaults.get_defaults().setup_complete or 0):
- frappe.db.sql('delete from `tabDesktop Icon` where standard=0')
+ frappe.db.delete("Desktop Icon", {"standard": 0})
# set standard as blocked and hidden if setting first active domain
if not frappe.flags.keep_desktop_icons:
diff --git a/frappe/desk/doctype/event/__init__.py b/frappe/desk/doctype/event/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/desk/doctype/event/__init__.py
+++ b/frappe/desk/doctype/event/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py
index 57c89eaf2e..d4c185e56f 100644
--- a/frappe/desk/doctype/event/event.py
+++ b/frappe/desk/doctype/event/event.py
@@ -1,5 +1,5 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
@@ -338,9 +338,8 @@ def delete_events(ref_type, ref_name, delete_event=False):
total_participants = frappe.get_all("Event Participants", filters={"parenttype": "Event", "parent": participation.parent})
if len(total_participants) <= 1:
- frappe.db.sql("DELETE FROM `tabEvent` WHERE `name` = %(name)s", {'name': participation.parent})
-
- frappe.db.sql("DELETE FROM `tabEvent Participants ` WHERE `name` = %(name)s", {'name': participation.name})
+ frappe.db.delete("Event", {"name": participation.parent})
+ frappe.db.delete("Event Participants", {"name": participation.name})
# Close events if ends_on or repeat_till is less than now_datetime
def set_status_of_events():
diff --git a/frappe/desk/doctype/event/test_event.py b/frappe/desk/doctype/event/test_event.py
index 77211946a9..6b7f6ee471 100644
--- a/frappe/desk/doctype/event/test_event.py
+++ b/frappe/desk/doctype/event/test_event.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
"""Use blog post test to test user permissions logic"""
import frappe
@@ -14,7 +14,7 @@ test_records = frappe.get_test_records('Event')
class TestEvent(unittest.TestCase):
def setUp(self):
- frappe.db.sql('delete from tabEvent')
+ frappe.db.delete("Event")
make_test_objects('Event', reset=True)
self.test_records = frappe.get_test_records('Event')
diff --git a/frappe/desk/doctype/event_participants/event_participants.py b/frappe/desk/doctype/event_participants/event_participants.py
index ca4fae9930..b834ba3a82 100644
--- a/frappe/desk/doctype/event_participants/event_participants.py
+++ b/frappe/desk/doctype/event_participants/event_participants.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from frappe.model.document import Document
class EventParticipants(Document):
diff --git a/frappe/chat/doctype/chat_room_user/__init__.py b/frappe/desk/doctype/form_tour/__init__.py
similarity index 100%
rename from frappe/chat/doctype/chat_room_user/__init__.py
rename to frappe/desk/doctype/form_tour/__init__.py
diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js
new file mode 100644
index 0000000000..8d70dcd3dc
--- /dev/null
+++ b/frappe/desk/doctype/form_tour/form_tour.js
@@ -0,0 +1,123 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Form Tour', {
+ setup: function(frm) {
+ if (!frm.doc.is_standard || frappe.boot.developer_mode) {
+ frm.trigger('setup_queries');
+ }
+ },
+
+ refresh(frm) {
+ if (frm.doc.is_standard && !frappe.boot.developer_mode) {
+ frm.trigger("disable_form");
+ }
+
+ frm.add_custom_button(__('Show Tour'), async () => {
+ const issingle = await check_if_single(frm.doc.reference_doctype);
+ let route_changed = null;
+
+ if (issingle) {
+ route_changed = frappe.set_route('Form', frm.doc.reference_doctype);
+ } else {
+ route_changed = frappe.set_route('Form', frm.doc.reference_doctype, 'new');
+ }
+ route_changed.then(() => {
+ const tour_name = frm.doc.name;
+ cur_frm.tour
+ .init({ tour_name })
+ .then(() => cur_frm.tour.start());
+ });
+ });
+ },
+
+ disable_form: function(frm) {
+ frm.set_read_only();
+ frm.fields
+ .filter((field) => field.has_input)
+ .forEach((field) => {
+ frm.set_df_property(field.df.fieldname, "read_only", "1");
+ });
+ frm.disable_save();
+ },
+
+ setup_queries(frm) {
+ frm.set_query("reference_doctype", function() {
+ return {
+ filters: {
+ istable: 0
+ }
+ };
+ });
+
+ frm.set_query("field", "steps", function() {
+ return {
+ query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list",
+ filters: {
+ doctype: frm.doc.reference_doctype,
+ hidden: 0
+ }
+ };
+ });
+
+ frm.set_query("parent_field", "steps", function() {
+ return {
+ query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list",
+ filters: {
+ doctype: frm.doc.reference_doctype,
+ fieldtype: "Table",
+ hidden: 0,
+ }
+ };
+ });
+
+ frm.trigger('reference_doctype');
+ },
+
+ reference_doctype(frm) {
+ if (!frm.doc.reference_doctype) return;
+
+ frappe.db.get_list('DocField', {
+ filters: {
+ parent: frm.doc.reference_doctype,
+ parenttype: 'DocType',
+ fieldtype: 'Table'
+ },
+ fields: ['options']
+ }).then(res => {
+ if (Array.isArray(res)) {
+ frm.child_doctypes = res.map(r => r.options);
+ }
+ });
+
+ }
+});
+
+frappe.ui.form.on('Form Tour Step', {
+ parent_field(frm, cdt, cdn) {
+ const child_row = locals[cdt][cdn];
+ frappe.model.set_value(cdt, cdn, 'field', '');
+ const field_control = get_child_field("steps", cdn, "field");
+ field_control.get_query = function() {
+ return {
+ query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list",
+ filters: {
+ doctype: child_row.child_doctype,
+ hidden: 0
+ }
+ };
+ };
+ }
+});
+
+function get_child_field(child_table, child_name, fieldname) {
+ // gets the field from grid row form
+ const grid = cur_frm.fields_dict[child_table].grid;
+ const grid_row = grid.grid_rows_by_docname[child_name];
+ return grid_row.grid_form.fields_dict[fieldname];
+}
+
+async function check_if_single(doctype) {
+ const { message } = await frappe.db.get_value('DocType', doctype, 'issingle');
+ return message.issingle || 0;
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json
new file mode 100644
index 0000000000..e4ea528fcc
--- /dev/null
+++ b/frappe/desk/doctype/form_tour/form_tour.json
@@ -0,0 +1,91 @@
+{
+ "actions": [],
+ "autoname": "field:title",
+ "creation": "2021-05-21 23:02:52.242721",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "title",
+ "reference_doctype",
+ "module",
+ "is_standard",
+ "save_on_complete",
+ "section_break_3",
+ "steps"
+ ],
+ "fields": [
+ {
+ "fieldname": "reference_doctype",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Reference Document",
+ "options": "DocType",
+ "reqd": 1
+ },
+ {
+ "depends_on": "reference_doctype",
+ "fieldname": "steps",
+ "fieldtype": "Table",
+ "label": "Steps",
+ "options": "Form Tour Step",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_3",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "label": "Title",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "save_on_complete",
+ "fieldtype": "Check",
+ "label": "Save on Completion"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_standard",
+ "fieldtype": "Check",
+ "label": "Is Standard"
+ },
+ {
+ "fetch_from": "reference_doctype.module",
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Module",
+ "options": "Module Def",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-06-06 20:32:54.068774",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Form Tour",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py
new file mode 100644
index 0000000000..82d47224dd
--- /dev/null
+++ b/frappe/desk/doctype/form_tour/form_tour.py
@@ -0,0 +1,62 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# License: MIT. See LICENSE
+
+import frappe
+from frappe.model.document import Document
+from frappe.modules.export_file import export_to_files
+
+class FormTour(Document):
+ def before_insert(self):
+ if not self.is_standard:
+ return
+
+ # while syncing, set proper docfield reference
+ for d in self.steps:
+ if not frappe.db.exists('DocField', d.field):
+ d.field = frappe.db.get_value('DocField', {
+ 'fieldname': d.fieldname, 'parent': self.reference_doctype, 'fieldtype': d.fieldtype
+ }, "name")
+
+ if d.is_table_field and not frappe.db.exists('DocField', d.parent_field):
+ d.parent_field = frappe.db.get_value('DocField', {
+ 'fieldname': d.parent_fieldname, 'parent': self.reference_doctype, 'fieldtype': 'Table'
+ }, "name")
+
+ def on_update(self):
+ if frappe.conf.developer_mode and self.is_standard:
+ export_to_files([['Form Tour', self.name]], self.module)
+
+ def before_export(self, doc):
+ for d in doc.steps:
+ d.field = ""
+ d.parent_field = ""
+
+@frappe.whitelist()
+@frappe.validate_and_sanitize_search_inputs
+def get_docfield_list(doctype, txt, searchfield, start, page_len, filters):
+ or_filters = [
+ ['fieldname', 'like', '%' + txt + '%'],
+ ['label', 'like', '%' + txt + '%'],
+ ['fieldtype', 'like', '%' + txt + '%']
+ ]
+
+ parent_doctype = filters.get('doctype')
+ fieldtype = filters.get('fieldtype')
+ if not fieldtype:
+ excluded_fieldtypes = ['Column Break']
+ excluded_fieldtypes += filters.get('excluded_fieldtypes', [])
+ fieldtype_filter = ['not in', excluded_fieldtypes]
+ else:
+ fieldtype_filter = fieldtype
+
+ docfields = frappe.get_all(
+ doctype,
+ fields=["name as value", "label", "fieldtype"],
+ filters={'parent': parent_doctype, 'fieldtype': fieldtype_filter},
+ or_filters=or_filters,
+ limit_start=start,
+ limit_page_length=page_len,
+ order_by="idx",
+ as_list=1,
+ )
+ return docfields
diff --git a/frappe/desk/doctype/form_tour/test_form_tour.py b/frappe/desk/doctype/form_tour/test_form_tour.py
new file mode 100644
index 0000000000..3670cbc218
--- /dev/null
+++ b/frappe/desk/doctype/form_tour/test_form_tour.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# License: MIT. See LICENSE
+
+# import frappe
+import unittest
+
+class TestFormTour(unittest.TestCase):
+ pass
diff --git a/frappe/chat/doctype/chat_token/__init__.py b/frappe/desk/doctype/form_tour_step/__init__.py
similarity index 100%
rename from frappe/chat/doctype/chat_token/__init__.py
rename to frappe/desk/doctype/form_tour_step/__init__.py
diff --git a/frappe/desk/doctype/form_tour_step/form_tour_step.json b/frappe/desk/doctype/form_tour_step/form_tour_step.json
new file mode 100644
index 0000000000..3b6c91a208
--- /dev/null
+++ b/frappe/desk/doctype/form_tour_step/form_tour_step.json
@@ -0,0 +1,151 @@
+{
+ "actions": [],
+ "creation": "2021-05-21 23:05:45.342114",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "is_table_field",
+ "section_break_2",
+ "parent_field",
+ "field",
+ "title",
+ "description",
+ "column_break_2",
+ "position",
+ "label",
+ "has_next_condition",
+ "next_step_condition",
+ "section_break_13",
+ "fieldname",
+ "parent_fieldname",
+ "fieldtype",
+ "child_doctype"
+ ],
+ "fields": [
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Title",
+ "reqd": 1
+ },
+ {
+ "columns": 4,
+ "fieldname": "description",
+ "fieldtype": "HTML Editor",
+ "in_list_view": 1,
+ "label": "Description",
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval: (!doc.is_table_field || (doc.is_table_field && doc.parent_field))",
+ "fieldname": "field",
+ "fieldtype": "Link",
+ "label": "Field",
+ "options": "DocField",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "field.fieldname",
+ "fieldname": "fieldname",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Fieldname",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "field.label",
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Label",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "Bottom",
+ "fieldname": "position",
+ "fieldtype": "Select",
+ "label": "Position",
+ "options": "Left\nLeft Center\nLeft Bottom\nTop\nTop Center\nTop Right\nRight\nRight Center\nRight Bottom\nBottom\nBottom Center\nBottom Right\nMid Center"
+ },
+ {
+ "depends_on": "has_next_condition",
+ "fieldname": "next_step_condition",
+ "fieldtype": "Code",
+ "label": "Next Step Condition",
+ "oldfieldname": "condition",
+ "options": "JS"
+ },
+ {
+ "default": "0",
+ "fieldname": "has_next_condition",
+ "fieldtype": "Check",
+ "label": "Has Next Condition"
+ },
+ {
+ "default": "0",
+ "fetch_from": "field.fieldtype",
+ "fieldname": "fieldtype",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Fieldtype",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "is_table_field",
+ "fieldtype": "Check",
+ "label": "Is Table Field"
+ },
+ {
+ "fieldname": "section_break_2",
+ "fieldtype": "Section Break"
+ },
+ {
+ "depends_on": "is_table_field",
+ "fieldname": "parent_field",
+ "fieldtype": "Link",
+ "label": "Parent Field",
+ "mandatory_depends_on": "is_table_field",
+ "options": "DocField"
+ },
+ {
+ "fieldname": "section_break_13",
+ "fieldtype": "Section Break",
+ "hidden": 1,
+ "label": "Hidden Fields"
+ },
+ {
+ "fetch_from": "parent_field.options",
+ "fieldname": "child_doctype",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Child Doctype",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "parent_field.fieldname",
+ "fieldname": "parent_fieldname",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Parent Fieldname",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-06-06 20:52:21.076972",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Form Tour Step",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/form_tour_step/form_tour_step.py b/frappe/desk/doctype/form_tour_step/form_tour_step.py
new file mode 100644
index 0000000000..bbc8edea08
--- /dev/null
+++ b/frappe/desk/doctype/form_tour_step/form_tour_step.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# License: MIT. See LICENSE
+
+# import frappe
+from frappe.model.document import Document
+
+class FormTourStep(Document):
+ pass
diff --git a/frappe/desk/doctype/global_search_doctype/global_search_doctype.py b/frappe/desk/doctype/global_search_doctype/global_search_doctype.py
index de8a48af01..30a31f959f 100644
--- a/frappe/desk/doctype/global_search_doctype/global_search_doctype.py
+++ b/frappe/desk/doctype/global_search_doctype/global_search_doctype.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/global_search_settings/global_search_settings.py b/frappe/desk/doctype/global_search_settings/global_search_settings.py
index 9112349c1b..9ffe9aaf06 100644
--- a/frappe/desk/doctype/global_search_settings/global_search_settings.py
+++ b/frappe/desk/doctype/global_search_settings/global_search_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/kanban_board/kanban_board.py b/frappe/desk/doctype/kanban_board/kanban_board.py
index 5100727f43..155a925fcf 100644
--- a/frappe/desk/doctype/kanban_board/kanban_board.py
+++ b/frappe/desk/doctype/kanban_board/kanban_board.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
import json
diff --git a/frappe/desk/doctype/kanban_board/test_kanban_board.py b/frappe/desk/doctype/kanban_board/test_kanban_board.py
index f9503d736a..f00446141a 100644
--- a/frappe/desk/doctype/kanban_board/test_kanban_board.py
+++ b/frappe/desk/doctype/kanban_board/test_kanban_board.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/desk/doctype/kanban_board_column/kanban_board_column.py b/frappe/desk/doctype/kanban_board_column/kanban_board_column.py
index aebba3351c..d919fd6aed 100644
--- a/frappe/desk/doctype/kanban_board_column/kanban_board_column.py
+++ b/frappe/desk/doctype/kanban_board_column/kanban_board_column.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/list_filter/list_filter.py b/frappe/desk/doctype/list_filter/list_filter.py
index 2467ae40a4..d2b01d301e 100644
--- a/frappe/desk/doctype/list_filter/list_filter.py
+++ b/frappe/desk/doctype/list_filter/list_filter.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe, json
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/list_view_settings/list_view_settings.py b/frappe/desk/doctype/list_view_settings/list_view_settings.py
index f4a288b7ba..78b56fe7d5 100644
--- a/frappe/desk/doctype/list_view_settings/list_view_settings.py
+++ b/frappe/desk/doctype/list_view_settings/list_view_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/list_view_settings/test_list_view_settings.py b/frappe/desk/doctype/list_view_settings/test_list_view_settings.py
index 00010d7604..85872dd36e 100644
--- a/frappe/desk/doctype/list_view_settings/test_list_view_settings.py
+++ b/frappe/desk/doctype/list_view_settings/test_list_view_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/desk/doctype/module_onboarding/module_onboarding.py b/frappe/desk/doctype/module_onboarding/module_onboarding.py
index 6f01e0fd8d..aa268c792c 100644
--- a/frappe/desk/doctype/module_onboarding/module_onboarding.py
+++ b/frappe/desk/doctype/module_onboarding/module_onboarding.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/module_onboarding/test_module_onboarding.py b/frappe/desk/doctype/module_onboarding/test_module_onboarding.py
index 39184401a1..42f472abc1 100644
--- a/frappe/desk/doctype/module_onboarding/test_module_onboarding.py
+++ b/frappe/desk/doctype/module_onboarding/test_module_onboarding.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/desk/doctype/note/note.json b/frappe/desk/doctype/note/note.json
index 8d476e83fe..69a9518ac4 100644
--- a/frappe/desk/doctype/note/note.json
+++ b/frappe/desk/doctype/note/note.json
@@ -1,322 +1,106 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 1,
- "beta": 0,
- "creation": "2013-05-24 13:41:00",
- "custom": 0,
- "description": "",
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Document",
- "editable_grid": 0,
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "title",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 1,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Title",
- "length": 0,
- "no_copy": 1,
- "permlevel": 0,
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "description": "",
- "fieldname": "public",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Public",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "public",
- "fieldname": "notify_on_login",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Notify users with a popup when they log in",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "default": "0",
- "depends_on": "notify_on_login",
- "description": "If enabled, users will be notified every time they login. If not enabled, users will only be notified once.",
- "fieldname": "notify_on_every_login",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Notify Users On Every Login",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.notify_on_login && doc.public",
- "fieldname": "expire_notification_on",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Expire Notification On",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 1,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "description": "Help: To link to another record in the system, use \"#Form/Note/[Note Name]\" as the Link URL. (don't use \"http://\")",
- "fieldname": "content",
- "fieldtype": "Text Editor",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 1,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Content",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 1,
- "columns": 0,
- "fieldname": "seen_by_section",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Seen By",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "seen_by",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Seen By Table",
- "length": 0,
- "no_copy": 0,
- "options": "Note Seen By",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-file-text",
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-09-21 15:15:44.909636",
- "modified_by": "Administrator",
- "module": "Desk",
- "name": "Note",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 0,
- "role": "All",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 1,
- "show_name_in_global_search": 0,
- "sort_order": "ASC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
- }
\ No newline at end of file
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2013-05-24 13:41:00",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "engine": "InnoDB",
+ "field_order": [
+ "title",
+ "public",
+ "notify_on_login",
+ "notify_on_every_login",
+ "expire_notification_on",
+ "content",
+ "seen_by_section",
+ "seen_by"
+ ],
+ "fields": [
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "in_global_search": 1,
+ "in_list_view": 1,
+ "label": "Title",
+ "no_copy": 1,
+ "print_hide": 1,
+ "reqd": 1
+ },
+ {
+ "bold": 1,
+ "default": "0",
+ "fieldname": "public",
+ "fieldtype": "Check",
+ "label": "Public",
+ "print_hide": 1
+ },
+ {
+ "bold": 1,
+ "default": "0",
+ "depends_on": "public",
+ "fieldname": "notify_on_login",
+ "fieldtype": "Check",
+ "label": "Notify users with a popup when they log in"
+ },
+ {
+ "bold": 1,
+ "default": "0",
+ "depends_on": "notify_on_login",
+ "description": "If enabled, users will be notified every time they login. If not enabled, users will only be notified once.",
+ "fieldname": "notify_on_every_login",
+ "fieldtype": "Check",
+ "label": "Notify Users On Every Login"
+ },
+ {
+ "depends_on": "eval:doc.notify_on_login && doc.public",
+ "fieldname": "expire_notification_on",
+ "fieldtype": "Date",
+ "label": "Expire Notification On",
+ "search_index": 1
+ },
+ {
+ "bold": 1,
+ "description": "Help: To link to another record in the system, use \"/app/note/[Note Name]\" as the Link URL. (don't use \"http://\")",
+ "fieldname": "content",
+ "fieldtype": "Text Editor",
+ "in_global_search": 1,
+ "label": "Content"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "seen_by_section",
+ "fieldtype": "Section Break",
+ "label": "Seen By"
+ },
+ {
+ "fieldname": "seen_by",
+ "fieldtype": "Table",
+ "label": "Seen By Table",
+ "options": "Note Seen By"
+ }
+ ],
+ "icon": "fa fa-file-text",
+ "idx": 1,
+ "links": [],
+ "modified": "2021-09-18 10:57:51.352643",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Note",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "All",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "ASC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/note/note.py b/frappe/desk/doctype/note/note.py
index 790f9a514c..ae7af07cd9 100644
--- a/frappe/desk/doctype/note/note.py
+++ b/frappe/desk/doctype/note/note.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/note/test_note.py b/frappe/desk/doctype/note/test_note.py
index 1bb1730357..ac2116c38a 100644
--- a/frappe/desk/doctype/note/test_note.py
+++ b/frappe/desk/doctype/note/test_note.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
@@ -8,9 +8,9 @@ test_records = frappe.get_test_records('Note')
class TestNote(unittest.TestCase):
def insert_note(self):
- frappe.db.sql('delete from tabVersion')
- frappe.db.sql('delete from tabNote')
- frappe.db.sql('delete from `tabNote Seen By`')
+ frappe.db.delete("Version")
+ frappe.db.delete("Note")
+ frappe.db.delete("Note Seen By")
return frappe.get_doc(dict(doctype='Note', title='test note',
content='test note content')).insert()
diff --git a/frappe/desk/doctype/note_seen_by/note_seen_by.py b/frappe/desk/doctype/note_seen_by/note_seen_by.py
index cec4628b20..01bee05a9f 100644
--- a/frappe/desk/doctype/note_seen_by/note_seen_by.py
+++ b/frappe/desk/doctype/note_seen_by/note_seen_by.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/notification_log/notification_log.json b/frappe/desk/doctype/notification_log/notification_log.json
index 9e802298e3..e188708277 100644
--- a/frappe/desk/doctype/notification_log/notification_log.json
+++ b/frappe/desk/doctype/notification_log/notification_log.json
@@ -120,7 +120,7 @@
"hide_toolbar": 1,
"in_create": 1,
"links": [],
- "modified": "2020-09-18 17:26:09.703215",
+ "modified": "2021-10-25 17:26:09.703215",
"modified_by": "Administrator",
"module": "Desk",
"name": "Notification Log",
@@ -139,6 +139,5 @@
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "subject",
- "track_changes": 1,
"track_seen": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py
index 414f272f59..12e628ada2 100644
--- a/frappe/desk/doctype/notification_log/notification_log.py
+++ b/frappe/desk/doctype/notification_log/notification_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
@@ -12,7 +12,10 @@ class NotificationLog(Document):
frappe.publish_realtime('notification', after_commit=True, user=self.for_user)
set_notifications_as_unseen(self.for_user)
if is_email_notifications_enabled_for_type(self.for_user, self.type):
- send_notification_email(self)
+ try:
+ send_notification_email(self)
+ except frappe.OutgoingEmailError:
+ frappe.log_error(message=frappe.get_traceback(), title=_("Failed to send notification email"))
def get_permission_query_conditions(for_user):
diff --git a/frappe/desk/doctype/notification_log/test_notification_log.py b/frappe/desk/doctype/notification_log/test_notification_log.py
index af4dee8df3..4c415a860c 100644
--- a/frappe/desk/doctype/notification_log/test_notification_log.py
+++ b/frappe/desk/doctype/notification_log/test_notification_log.py
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
+from frappe.core.doctype.user.user import get_system_users
from frappe.desk.form.assign_to import add as assign_task
import unittest
@@ -54,7 +55,4 @@ def get_todo():
return frappe.get_cached_doc('ToDo', res[0].name)
def get_user():
- users = frappe.db.get_all('User',
- filters={'name': ('not in', ['Administrator', 'Guest'])},
- fields='name', limit=1)
- return users[0].name
+ return get_system_users(limit=1)[0]
diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py
index eb3a16435f..cf6bb2d78d 100644
--- a/frappe/desk/doctype/notification_settings/notification_settings.py
+++ b/frappe/desk/doctype/notification_settings/notification_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/notification_subscribed_document/notification_subscribed_document.py b/frappe/desk/doctype/notification_subscribed_document/notification_subscribed_document.py
index 6931e77754..1fdba22779 100644
--- a/frappe/desk/doctype/notification_subscribed_document/notification_subscribed_document.py
+++ b/frappe/desk/doctype/notification_subscribed_document/notification_subscribed_document.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py
index d8d5fe0953..5662523a9d 100644
--- a/frappe/desk/doctype/number_card/number_card.py
+++ b/frappe/desk/doctype/number_card/number_card.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/number_card/test_number_card.py b/frappe/desk/doctype/number_card/test_number_card.py
index c395f5f915..cc92e63341 100644
--- a/frappe/desk/doctype/number_card/test_number_card.py
+++ b/frappe/desk/doctype/number_card/test_number_card.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/desk/doctype/number_card_link/number_card_link.py b/frappe/desk/doctype/number_card_link/number_card_link.py
index 6c16f45f4b..0b55ae6dcd 100644
--- a/frappe/desk/doctype/number_card_link/number_card_link.py
+++ b/frappe/desk/doctype/number_card_link/number_card_link.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/onboarding_permission/onboarding_permission.py b/frappe/desk/doctype/onboarding_permission/onboarding_permission.py
index 40d3dc33b1..a0e87c3067 100644
--- a/frappe/desk/doctype/onboarding_permission/onboarding_permission.py
+++ b/frappe/desk/doctype/onboarding_permission/onboarding_permission.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/onboarding_permission/test_onboarding_permission.py b/frappe/desk/doctype/onboarding_permission/test_onboarding_permission.py
index 80b166de0a..c13fb29678 100644
--- a/frappe/desk/doctype/onboarding_permission/test_onboarding_permission.py
+++ b/frappe/desk/doctype/onboarding_permission/test_onboarding_permission.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.py b/frappe/desk/doctype/onboarding_step/onboarding_step.py
index 10bd8926ce..45e0ca34fd 100644
--- a/frappe/desk/doctype/onboarding_step/onboarding_step.py
+++ b/frappe/desk/doctype/onboarding_step/onboarding_step.py
@@ -1,11 +1,27 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
-# import frappe
+import frappe
+from frappe import _
+import json
from frappe.model.document import Document
class OnboardingStep(Document):
def before_export(self, doc):
doc.is_complete = 0
doc.is_skipped = 0
+
+
+@frappe.whitelist()
+def get_onboarding_steps(ob_steps):
+ steps = []
+ for s in json.loads(ob_steps):
+ doc = frappe.get_doc('Onboarding Step', s.get('step'))
+ step = doc.as_dict().copy()
+ step.label = _(doc.title)
+ if step.action == "Create Entry":
+ step.is_submittable = frappe.db.get_value("DocType", step.reference_document, 'is_submittable', cache=True)
+ steps.append(step)
+
+ return steps
diff --git a/frappe/desk/doctype/onboarding_step/test_onboarding_step.py b/frappe/desk/doctype/onboarding_step/test_onboarding_step.py
index 2425577478..b0651da4da 100644
--- a/frappe/desk/doctype/onboarding_step/test_onboarding_step.py
+++ b/frappe/desk/doctype/onboarding_step/test_onboarding_step.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/desk/doctype/onboarding_step_map/onboarding_step_map.py b/frappe/desk/doctype/onboarding_step_map/onboarding_step_map.py
index c79244c4ad..7c20e220db 100644
--- a/frappe/desk/doctype/onboarding_step_map/onboarding_step_map.py
+++ b/frappe/desk/doctype/onboarding_step_map/onboarding_step_map.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/route_history/route_history.json b/frappe/desk/doctype/route_history/route_history.json
index 7390aa011b..09db2320ca 100644
--- a/frappe/desk/doctype/route_history/route_history.json
+++ b/frappe/desk/doctype/route_history/route_history.json
@@ -88,7 +88,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2018-10-05 13:26:03.106050",
+ "modified": "2021-10-25 13:26:03.106050",
"modified_by": "Administrator",
"module": "Desk",
"name": "Route History",
@@ -121,7 +121,6 @@
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
- "track_changes": 1,
"track_seen": 0,
"track_views": 0
-}
\ No newline at end of file
+}
diff --git a/frappe/desk/doctype/route_history/route_history.py b/frappe/desk/doctype/route_history/route_history.py
index b82077f485..01184fcc3a 100644
--- a/frappe/desk/doctype/route_history/route_history.py
+++ b/frappe/desk/doctype/route_history/route_history.py
@@ -1,6 +1,5 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
+# Copyright (c) 2021, Frappe Technologies and contributors
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
@@ -8,6 +7,7 @@ from frappe.model.document import Document
class RouteHistory(Document):
pass
+
def flush_old_route_records():
"""Deletes all route records except last 500 records per user"""
@@ -24,19 +24,14 @@ def flush_old_route_records():
for user in users:
user = user[0]
last_record_to_keep = frappe.db.get_all('Route History',
- filters={
- 'user': user,
- },
+ filters={'user': user},
limit=1,
limit_start=500,
fields=['modified'],
- order_by='modified desc')
+ order_by='modified desc'
+ )
- frappe.db.sql('''
- DELETE
- FROM `tabRoute History`
- WHERE `modified` <= %(modified)s and `user`=%(modified)s
- ''', {
- "modified": last_record_to_keep[0].modified,
+ frappe.db.delete("Route History", {
+ "modified": ("<=", last_record_to_keep[0].modified),
"user": user
- })
\ No newline at end of file
+ })
diff --git a/frappe/desk/doctype/system_console/system_console.js b/frappe/desk/doctype/system_console/system_console.js
index c7eac39490..0fe3932671 100644
--- a/frappe/desk/doctype/system_console/system_console.js
+++ b/frappe/desk/doctype/system_console/system_console.js
@@ -5,17 +5,100 @@ frappe.ui.form.on('System Console', {
onload: function(frm) {
frappe.ui.keys.add_shortcut({
shortcut: 'shift+enter',
- action: () => frm.execute_action('Execute'),
+ action: () => frm.page.btn_primary.trigger('click'),
page: frm.page,
description: __('Execute Console script'),
ignore_inputs: true,
});
+ frm.set_value("type", "Python");
},
refresh: function(frm) {
frm.disable_save();
- frm.page.set_primary_action(__("Execute"), () => {
- frm.execute_action('Execute');
+ frm.page.set_primary_action(__("Execute"), $btn => {
+ $btn.text(__("Executing..."));
+ return frm
+ .execute_action("Execute")
+ .then(() => frm.trigger("render_sql_output"))
+ .finally(() => $btn.text(__("Execute")));
+ });
+ },
+
+ type: function(frm) {
+ if (frm.doc.type == "Python") {
+ frm.set_value("output", "");
+ if (frm.sql_output) {
+ frm.sql_output.destroy();
+ frm.get_field("sql_output").html("");
+ }
+ }
+ },
+
+ render_sql_output: function(frm) {
+ if (frm.doc.type !== "SQL") return;
+ if (frm.sql_output) {
+ frm.sql_output.destroy();
+ frm.get_field("sql_output").html("");
+ }
+
+ if (frm.doc.output.startsWith("Traceback")) {
+ return;
+ }
+
+ let result = JSON.parse(frm.doc.output);
+ frm.set_value("output", `${result.length} ${result.length == 1 ? 'row' : 'rows'}`);
+
+ if (result.length) {
+ let columns = Object.keys(result[0]);
+ frm.sql_output = new DataTable(
+ frm.get_field("sql_output").$wrapper.get(0),
+ {
+ columns,
+ data: result
+ }
+ );
+ }
+ },
+
+ show_processlist: function(frm) {
+ if (frm.doc.show_processlist) {
+ // keep refreshing every 5 seconds
+ frm.events.refresh_processlist(frm);
+ frm.processlist_interval = setInterval(() => frm.events.refresh_processlist(frm), 5000);
+ } else {
+ if (frm.processlist_interval) {
+
+ // end it
+ clearInterval(frm.processlist_interval);
+ frm.get_field("processlist").html('');
+ }
+ }
+ },
+
+ refresh_processlist: function(frm) {
+ let timestamp = new Date();
+ frappe.call('frappe.desk.doctype.system_console.system_console.show_processlist').then(r => {
+ let rows = '';
+ for (let row of r.message) {
+ rows += `
+ | ${row.Id} |
+ ${row.Time} |
+ ${row.State} |
+ ${row.Info} |
+ ${row.Progress} |
+
`
+ }
+ frm.get_field('processlist').html(`
+ Requested on: ${timestamp}
+
+
+ | Id
+ | Time
+ | State
+ | Info
+ | Progress
+ |
+ ${rows}`);
});
}
});
diff --git a/frappe/desk/doctype/system_console/system_console.json b/frappe/desk/doctype/system_console/system_console.json
index 14e36e6fd3..657e9df89d 100644
--- a/frappe/desk/doctype/system_console/system_console.json
+++ b/frappe/desk/doctype/system_console/system_console.json
@@ -17,9 +17,15 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
+ "execute_section",
+ "type",
"console",
"commit",
- "output"
+ "output",
+ "sql_output",
+ "database_processes_section",
+ "show_processlist",
+ "processlist"
],
"fields": [
{
@@ -40,13 +46,47 @@
"fieldname": "commit",
"fieldtype": "Check",
"label": "Commit"
+ },
+ {
+ "fieldname": "execute_section",
+ "fieldtype": "Section Break",
+ "label": "Execute"
+ },
+ {
+ "fieldname": "database_processes_section",
+ "fieldtype": "Section Break",
+ "label": "Database Processes"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_processlist",
+ "fieldtype": "Check",
+ "label": "Show Processlist"
+ },
+ {
+ "fieldname": "processlist",
+ "fieldtype": "HTML",
+ "label": "processlist"
+ },
+ {
+ "default": "Python",
+ "fieldname": "type",
+ "fieldtype": "Select",
+ "label": "Type",
+ "options": "Python\nSQL"
+ },
+ {
+ "depends_on": "eval:doc.type == 'SQL'",
+ "fieldname": "sql_output",
+ "fieldtype": "HTML",
+ "label": "SQL Output"
}
],
"hide_toolbar": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2020-08-21 14:44:35.296877",
+ "modified": "2021-09-15 17:17:44.844767",
"modified_by": "Administrator",
"module": "Desk",
"name": "System Console",
@@ -65,4 +105,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/system_console/system_console.py b/frappe/desk/doctype/system_console/system_console.py
index e2b5656bc0..107ab2f932 100644
--- a/frappe/desk/doctype/system_console/system_console.py
+++ b/frappe/desk/doctype/system_console/system_console.py
@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import json
import frappe
-from frappe.utils.safe_exec import safe_exec
+from frappe.utils.safe_exec import safe_exec, read_sql
from frappe.model.document import Document
class SystemConsole(Document):
@@ -13,8 +13,11 @@ class SystemConsole(Document):
frappe.only_for('System Manager')
try:
frappe.debug_log = []
- safe_exec(self.console)
- self.output = '\n'.join(frappe.debug_log)
+ if self.type == 'Python':
+ safe_exec(self.console)
+ self.output = '\n'.join(frappe.debug_log)
+ elif self.type == 'SQL':
+ self.output = frappe.as_json(read_sql(self.console, as_dict=1))
except: # noqa: E722
self.output = frappe.get_traceback()
@@ -33,4 +36,9 @@ class SystemConsole(Document):
def execute_code(doc):
console = frappe.get_doc(json.loads(doc))
console.run()
- return console.as_dict()
\ No newline at end of file
+ return console.as_dict()
+
+@frappe.whitelist()
+def show_processlist():
+ frappe.only_for('System Manager')
+ return frappe.db.sql('show full processlist', as_dict=1)
diff --git a/frappe/desk/doctype/system_console/test_system_console.py b/frappe/desk/doctype/system_console/test_system_console.py
index 743c2d6dde..fa7c577faa 100644
--- a/frappe/desk/doctype/system_console/test_system_console.py
+++ b/frappe/desk/doctype/system_console/test_system_console.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py
index 4ea5c9cd7e..381c24a765 100644
--- a/frappe/desk/doctype/tag/tag.py
+++ b/frappe/desk/doctype/tag/tag.py
@@ -1,10 +1,10 @@
-# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
from frappe.utils import unique
+from frappe.query_builder import DocType
class Tag(Document):
pass
@@ -12,7 +12,8 @@ class Tag(Document):
def check_user_tags(dt):
"if the user does not have a tags column, then it creates one"
try:
- frappe.db.sql("select `_user_tags` from `tab%s` limit 1" % dt)
+ doctype = DocType(dt)
+ frappe.qb.from_(doctype).select(doctype._user_tags).limit(1).run()
except Exception as e:
if frappe.db.is_column_missing(e):
DocTags(dt).setup()
@@ -43,10 +44,12 @@ def remove_tag(tag, dt, dn):
@frappe.whitelist()
def get_tagged_docs(doctype, tag):
frappe.has_permission(doctype, throw=True)
-
- return frappe.db.sql("""SELECT name
- FROM `tab{0}`
- WHERE _user_tags LIKE '%{1}%'""".format(doctype, tag))
+ doctype = DocType(doctype)
+ return (
+ frappe.qb.from_(doctype)
+ .where(doctype._user_tags.like(tag))
+ .select(doctype.name)
+ ).run()
@frappe.whitelist()
def get_tags(doctype, txt):
@@ -123,45 +126,41 @@ def delete_tags_for_document(doc):
if not frappe.db.table_exists("Tag Link"):
return
- frappe.db.sql("""DELETE FROM `tabTag Link` WHERE `document_type`=%s AND `document_name`=%s""", (doc.doctype, doc.name))
+ frappe.db.delete("Tag Link", {
+ "document_type": doc.doctype,
+ "document_name": doc.name
+ })
def update_tags(doc, tags):
- """
- Adds tags for documents
- :param doc: Document to be added to global tags
- """
+ """Adds tags for documents
+ :param doc: Document to be added to global tags
+ """
new_tags = {tag.strip() for tag in tags.split(",") if tag}
-
- for tag in new_tags:
- if not frappe.db.exists("Tag Link", {"parenttype": doc.doctype, "parent": doc.name, "tag": tag}):
- frappe.get_doc({
- "doctype": "Tag Link",
- "document_type": doc.doctype,
- "document_name": doc.name,
- "parenttype": doc.doctype,
- "parent": doc.name,
- "title": doc.get_title() or '',
- "tag": tag
- }).insert(ignore_permissions=True)
-
existing_tags = [tag.tag for tag in frappe.get_list("Tag Link", filters={
"document_type": doc.doctype,
"document_name": doc.name
}, fields=["tag"])]
- deleted_tags = get_deleted_tags(new_tags, existing_tags)
+ added_tags = set(new_tags) - set(existing_tags)
+ for tag in added_tags:
+ frappe.get_doc({
+ "doctype": "Tag Link",
+ "document_type": doc.doctype,
+ "document_name": doc.name,
+ "parenttype": doc.doctype,
+ "parent": doc.name,
+ "title": doc.get_title() or '',
+ "tag": tag
+ }).insert(ignore_permissions=True)
- if deleted_tags:
- for tag in deleted_tags:
- delete_tag_for_document(doc.doctype, doc.name, tag)
-
-def get_deleted_tags(new_tags, existing_tags):
-
- return list(set(existing_tags) - set(new_tags))
-
-def delete_tag_for_document(dt, dn, tag):
- frappe.db.sql("""DELETE FROM `tabTag Link` WHERE `document_type`=%s AND `document_name`=%s AND tag=%s""", (dt, dn, tag))
+ deleted_tags = list(set(existing_tags) - set(new_tags))
+ for tag in deleted_tags:
+ frappe.db.delete("Tag Link", {
+ "document_type": doc.doctype,
+ "document_name": doc.name,
+ "tag": tag
+ })
@frappe.whitelist()
def get_documents_for_tag(tag):
diff --git a/frappe/desk/doctype/tag/test_tag.py b/frappe/desk/doctype/tag/test_tag.py
index 442a891fd8..b9c6e0b744 100644
--- a/frappe/desk/doctype/tag/test_tag.py
+++ b/frappe/desk/doctype/tag/test_tag.py
@@ -1,8 +1,26 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
-# import frappe
import unittest
+import frappe
+
+from frappe.desk.reportview import get_stats
+from frappe.desk.doctype.tag.tag import add_tag
class TestTag(unittest.TestCase):
- pass
+ def setUp(self) -> None:
+ frappe.db.delete("Tag")
+ frappe.db.sql("UPDATE `tabDocType` set _user_tags=''")
+
+ def test_tag_count_query(self):
+ self.assertDictEqual(get_stats('["_user_tags"]', 'DocType'),
+ {'_user_tags': [['No Tags', frappe.db.count('DocType')]]})
+ add_tag('Standard', 'DocType', 'User')
+ add_tag('Standard', 'DocType', 'ToDo')
+
+ # count with no filter
+ self.assertDictEqual(get_stats('["_user_tags"]', 'DocType'),
+ {'_user_tags': [['Standard', 2], ['No Tags', frappe.db.count('DocType') - 2]]})
+
+ # count with child table field filter
+ self.assertDictEqual(get_stats('["_user_tags"]',
+ 'DocType',
+ filters='[["DocField", "fieldname", "like", "%last_name%"], ["DocType", "name", "like", "%use%"]]'),
+ {'_user_tags': [['Standard', 1], ['No Tags', 0]]})
\ No newline at end of file
diff --git a/frappe/desk/doctype/tag_link/tag_link.json b/frappe/desk/doctype/tag_link/tag_link.json
index 00a7349c5c..9142279fa3 100644
--- a/frappe/desk/doctype/tag_link/tag_link.json
+++ b/frappe/desk/doctype/tag_link/tag_link.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2019-09-24 13:25:36.435685",
"doctype": "DocType",
"editable_grid": 1,
@@ -44,7 +45,8 @@
"read_only": 1
}
],
- "modified": "2019-10-03 16:42:35.932409",
+ "links": [],
+ "modified": "2021-09-20 16:53:37.217998",
"modified_by": "Administrator",
"module": "Desk",
"name": "Tag Link",
@@ -61,6 +63,17 @@
"role": "System Manager",
"share": 1,
"write": 1
+ },
+ {
+ "create": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "All",
+ "share": 1,
+ "write": 1
}
],
"read_only": 1,
diff --git a/frappe/desk/doctype/tag_link/tag_link.py b/frappe/desk/doctype/tag_link/tag_link.py
index 4c5149f42c..d07894989d 100644
--- a/frappe/desk/doctype/tag_link/tag_link.py
+++ b/frappe/desk/doctype/tag_link/tag_link.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/tag_link/test_tag_link.py b/frappe/desk/doctype/tag_link/test_tag_link.py
index 297ee3cc96..fa6a22903f 100644
--- a/frappe/desk/doctype/tag_link/test_tag_link.py
+++ b/frappe/desk/doctype/tag_link/test_tag_link.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/desk/doctype/todo/__init__.py b/frappe/desk/doctype/todo/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/desk/doctype/todo/__init__.py
+++ b/frappe/desk/doctype/todo/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/desk/doctype/todo/test_todo.py b/frappe/desk/doctype/todo/test_todo.py
index b38e4a059a..34d3cee191 100644
--- a/frappe/desk/doctype/todo/test_todo.py
+++ b/frappe/desk/doctype/todo/test_todo.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
from frappe.model.db_query import DatabaseQuery
@@ -14,7 +14,7 @@ class TestToDo(unittest.TestCase):
todo = frappe.get_doc(dict(doctype='ToDo', description='test todo',
assigned_by='Administrator')).insert()
- frappe.db.sql('delete from `tabDeleted Document`')
+ frappe.db.delete("Deleted Document")
todo.delete()
deleted = frappe.get_doc('Deleted Document', dict(deleted_doctype=todo.doctype, deleted_name=todo.name))
@@ -27,7 +27,7 @@ class TestToDo(unittest.TestCase):
frappe.db.get_value('User', todo.assigned_by, 'full_name'))
def test_fetch_setup(self):
- frappe.db.sql('delete from tabToDo')
+ frappe.db.delete("ToDo")
todo_meta = frappe.get_doc('DocType', 'ToDo')
todo_meta.get('fields', dict(fieldname='assigned_by_full_name'))[0].fetch_from = ''
@@ -104,8 +104,8 @@ class TestToDo(unittest.TestCase):
clear_permissions_cache('ToDo')
frappe.db.rollback()
-def test_fetch_if_empty(self):
- frappe.db.sql('delete from tabToDo')
+ def test_fetch_if_empty(self):
+ frappe.db.delete("ToDo")
# Allow user changes
todo_meta = frappe.get_doc('DocType', 'ToDo')
@@ -122,9 +122,8 @@ def test_fetch_if_empty(self):
self.assertEqual(todo.assigned_by_full_name, 'Admin')
# Overwrite user changes
- todo_meta = frappe.get_doc('DocType', 'ToDo')
- todo_meta.get('fields', dict(fieldname='assigned_by_full_name'))[0].fetch_if_empty = 0
- todo_meta.save()
+ todo.meta.get('fields', dict(fieldname='assigned_by_full_name'))[0].fetch_if_empty = 0
+ todo.meta.save()
todo.reload()
todo.save()
diff --git a/frappe/desk/doctype/todo/todo.py b/frappe/desk/doctype/todo/todo.py
index 4696563445..6f3f4160e6 100644
--- a/frappe/desk/doctype/todo/todo.py
+++ b/frappe/desk/doctype/todo/todo.py
@@ -1,15 +1,17 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import json
from frappe.model.document import Document
-from frappe.utils import get_fullname
+from frappe.utils import get_fullname, parse_addr
exclude_from_linked_with = True
class ToDo(Document):
+ DocType = 'ToDo'
+
def validate(self):
self._assignment = None
if self.is_new():
@@ -27,8 +29,15 @@ class ToDo(Document):
else:
# NOTE the previous value is only available in validate method
if self.get_db_value("status") != self.status:
+ if self.owner == frappe.session.user:
+ removal_message = frappe._("{0} removed their assignment.").format(
+ get_fullname(frappe.session.user))
+ else:
+ removal_message = frappe._("Assignment of {0} removed by {1}").format(
+ get_fullname(self.owner), get_fullname(frappe.session.user))
+
self._assignment = {
- "text": frappe._("Assignment closed by {0}").format(get_fullname(frappe.session.user)),
+ "text": removal_message,
"comment_type": "Assignment Completed"
}
@@ -39,13 +48,7 @@ class ToDo(Document):
self.update_in_reference()
def on_trash(self):
- # unlink todo from linked comments
- frappe.db.sql("""
- delete from `tabCommunication Link`
- where link_doctype=%(doctype)s and link_name=%(name)s""", {
- "doctype": self.doctype, "name": self.name
- })
-
+ self.delete_communication_links()
self.update_in_reference()
def add_assign_comment(self, text, comment_type):
@@ -54,6 +57,13 @@ class ToDo(Document):
frappe.get_doc(self.reference_type, self.reference_name).add_comment(comment_type, text)
+ def delete_communication_links(self):
+ # unlink todo from linked comments
+ return frappe.db.delete("Communication Link", {
+ "link_doctype": self.doctype,
+ "link_name": self.name
+ })
+
def update_in_reference(self):
if not (self.reference_type and self.reference_name):
return
@@ -84,6 +94,13 @@ class ToDo(Document):
else:
raise
+ @classmethod
+ def get_owners(cls, filters=None):
+ """Returns list of owners after applying filters on todo's.
+ """
+ rows = frappe.get_all(cls.DocType, filters=filters or {}, fields=['owner'])
+ return [parse_addr(row.owner)[1] for row in rows if row.owner]
+
# NOTE: todo is viewable if a user is an owner, or set as assigned_to value, or has any role that is allowed to access ToDo doctype.
def on_doctype_update():
frappe.db.add_index("ToDo", ["reference_type", "reference_name"])
diff --git a/frappe/desk/doctype/workspace/test_workspace.py b/frappe/desk/doctype/workspace/test_workspace.py
index 619b3608eb..6c16e69afe 100644
--- a/frappe/desk/doctype/workspace/test_workspace.py
+++ b/frappe/desk/doctype/workspace/test_workspace.py
@@ -1,8 +1,95 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
-# import frappe
+# License: MIT. See LICENSE
+import frappe
import unittest
-
class TestWorkspace(unittest.TestCase):
- pass
+ def setUp(self):
+ create_module("Test Module")
+
+ def tearDown(self):
+ frappe.db.delete("Workspace", {"module": "Test Module"})
+ frappe.db.delete("DocType", {"module": "Test Module"})
+ frappe.delete_doc("Module Def", "Test Module")
+
+ # TODO: FIX ME - flaky test!!!
+ # def test_workspace_with_cards_specific_to_a_country(self):
+ # workspace = create_workspace()
+ # insert_card(workspace, "Card Label 1", "DocType 1", "DocType 2", "France")
+ # insert_card(workspace, "Card Label 2", "DocType A", "DocType B")
+
+ # workspace.insert(ignore_if_duplicate = True)
+
+ # cards = workspace.get_link_groups()
+
+ # if frappe.get_system_settings('country') == "France":
+ # self.assertEqual(len(cards), 2)
+ # else:
+ # self.assertEqual(len(cards), 1)
+
+def create_module(module_name):
+ module = frappe.get_doc({
+ "doctype": "Module Def",
+ "module_name": module_name,
+ "app_name": "frappe"
+ })
+ module.insert(ignore_if_duplicate = True)
+
+ return module
+
+def create_workspace(**args):
+ workspace = frappe.new_doc("Workspace")
+ args = frappe._dict(args)
+
+ workspace.name = args.name or "Test Workspace"
+ workspace.label = args.label or "Test Workspace"
+ workspace.category = args.category or "Modules"
+ workspace.is_standard = args.is_standard or 1
+ workspace.module = "Test Module"
+
+ return workspace
+
+def insert_card(workspace, card_label, doctype1, doctype2, country=None):
+ workspace.append("links", {
+ "type": "Card Break",
+ "label": card_label,
+ "only_for": country
+ })
+
+ create_doctype(doctype1, "Test Module")
+ workspace.append("links", {
+ "type": "Link",
+ "label": doctype1,
+ "only_for": country,
+ "link_type": "DocType",
+ "link_to": doctype1
+ })
+
+ create_doctype(doctype2, "Test Module")
+ workspace.append("links", {
+ "type": "Link",
+ "label": doctype2,
+ "only_for": country,
+ "link_type": "DocType",
+ "link_to": doctype2
+ })
+
+def create_doctype(doctype_name, module):
+ frappe.get_doc({
+ 'doctype': 'DocType',
+ 'name': doctype_name,
+ 'module': module,
+ 'custom': 1,
+ 'autoname': 'field:title',
+ 'fields': [
+ {'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'},
+ {'label': 'Description', 'fieldname': 'description', 'fieldtype': 'Small Text'},
+ {'label': 'Date', 'fieldname': 'date', 'fieldtype': 'Date'},
+ {'label': 'Duration', 'fieldname': 'duration', 'fieldtype': 'Duration'},
+ {'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'},
+ {'label': 'Number', 'fieldname': 'another_number', 'fieldtype': 'Int'}
+ ],
+ 'permissions': [
+ {'role': 'System Manager'}
+ ]
+ }).insert(ignore_if_duplicate = True)
diff --git a/frappe/desk/doctype/workspace/workspace.js b/frappe/desk/doctype/workspace/workspace.js
index 19d429f9f6..5377470343 100644
--- a/frappe/desk/doctype/workspace/workspace.js
+++ b/frappe/desk/doctype/workspace/workspace.js
@@ -8,14 +8,9 @@ frappe.ui.form.on('Workspace', {
refresh: function(frm) {
frm.enable_save();
- frm.get_field("is_standard").toggle(frappe.boot.developer_mode);
- frm.get_field("developer_mode_only").toggle(frappe.boot.developer_mode);
- if (frm.doc.for_user) {
- frm.set_df_property("extends", "read_only", true);
- }
-
- if (frm.doc.for_user || (frm.doc.is_standard && !frappe.boot.developer_mode)) {
+ if (frm.doc.for_user || (frm.doc.public && !frm.has_perm('write') &&
+ !frappe.user.has_role('Workspace Manager'))) {
frm.trigger('disable_form');
}
},
diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json
index 386267b699..04975c69e3 100644
--- a/frappe/desk/doctype/workspace/workspace.json
+++ b/frappe/desk/doctype/workspace/workspace.json
@@ -8,37 +8,32 @@
"engine": "InnoDB",
"field_order": [
"label",
+ "title",
+ "sequence_id",
"for_user",
- "extends",
+ "parent_page",
"module",
- "category",
+ "column_break_3",
"icon",
"restrict_to_domain",
- "onboarding",
- "column_break_3",
- "extends_another_page",
- "is_default",
- "is_standard",
- "developer_mode_only",
- "disable_user_customization",
- "pin_to_top",
- "pin_to_bottom",
"hide_custom",
+ "public",
+ "content",
"section_break_2",
- "charts_label",
"charts",
"section_break_15",
- "shortcuts_label",
"shortcuts",
"section_break_18",
- "cards_label",
- "links"
+ "links",
+ "roles_section",
+ "roles"
],
"fields": [
{
"fieldname": "label",
"fieldtype": "Data",
"label": "Name",
+ "reqd": 1,
"unique": 1
},
{
@@ -55,7 +50,6 @@
"options": "Workspace Chart"
},
{
- "depends_on": "eval:!doc.extends_another_page || !doc.is_standard || frappe.boot.developer_mode",
"fieldname": "shortcuts",
"fieldtype": "Table",
"label": "Shortcuts",
@@ -66,7 +60,6 @@
"fieldtype": "Link",
"label": "Restrict to Domain",
"options": "Domain",
- "read_only_depends_on": "eval:doc.extends_another_page == 0",
"search_index": 1
},
{
@@ -81,64 +74,6 @@
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
- {
- "fieldname": "category",
- "fieldtype": "Select",
- "label": "Category",
- "options": "Modules\nDomains\nPlaces\nAdministration",
- "read_only_depends_on": "eval:doc.extends_another_page == 1",
- "search_index": 1
- },
- {
- "default": "0",
- "depends_on": "eval:doc.extends_another_page == 0",
- "fieldname": "developer_mode_only",
- "fieldtype": "Check",
- "label": "Developer Mode Only",
- "search_index": 1
- },
- {
- "default": "0",
- "depends_on": "eval:doc.pin_to_bottom!=1 && doc.extends_another_page == 0",
- "fieldname": "pin_to_top",
- "fieldtype": "Check",
- "label": "Pin To Top",
- "search_index": 1
- },
- {
- "default": "0",
- "depends_on": "eval:doc.extends_another_page == 0",
- "fieldname": "disable_user_customization",
- "fieldtype": "Check",
- "label": "Disable User Customization",
- "search_index": 1
- },
- {
- "default": "0",
- "depends_on": "eval:doc.pin_to_top!=1 && doc.extends_another_page == 0",
- "fieldname": "pin_to_bottom",
- "fieldtype": "Check",
- "label": "Pin To Bottom",
- "search_index": 1
- },
- {
- "depends_on": "eval:!doc.extends_another_page || !doc.is_standard",
- "fieldname": "charts_label",
- "fieldtype": "Data",
- "label": "Label"
- },
- {
- "depends_on": "eval:!doc.extends_another_page || !doc.is_standard",
- "fieldname": "shortcuts_label",
- "fieldtype": "Data",
- "label": "Label"
- },
- {
- "depends_on": "eval:!doc.extends_another_page || !doc.is_standard",
- "fieldname": "cards_label",
- "fieldtype": "Data",
- "label": "Label"
- },
{
"collapsible": 1,
"collapsible_depends_on": "shortcuts",
@@ -153,43 +88,12 @@
"fieldtype": "Section Break",
"label": "Link Cards"
},
- {
- "default": "0",
- "fieldname": "is_standard",
- "fieldtype": "Check",
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Is Standard",
- "search_index": 1
- },
- {
- "default": "0",
- "fieldname": "extends_another_page",
- "fieldtype": "Check",
- "label": "Extends Another Page",
- "search_index": 1
- },
- {
- "depends_on": "eval:doc.extends_another_page == 1 || doc.for_user",
- "fieldname": "extends",
- "fieldtype": "Link",
- "in_standard_filter": 1,
- "label": "Extends",
- "options": "Workspace",
- "search_index": 1
- },
{
"fieldname": "for_user",
"fieldtype": "Data",
"label": "For User",
"read_only": 1
},
- {
- "fieldname": "onboarding",
- "fieldtype": "Link",
- "label": "Onboarding",
- "options": "Module Onboarding"
- },
{
"default": "0",
"description": "Checking this will hide custom doctypes and reports cards in Links section",
@@ -199,7 +103,7 @@
},
{
"fieldname": "icon",
- "fieldtype": "Data",
+ "fieldtype": "Icon",
"label": "Icon"
},
{
@@ -209,19 +113,56 @@
"options": "Workspace Link"
},
{
- "default": "0",
- "depends_on": "extends_another_page",
- "description": "Sets the current page as default for all users",
- "fieldname": "is_default",
- "fieldtype": "Check",
- "label": "Is Default"
- }
+ "default": "0",
+ "fieldname": "public",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Public",
+ "search_index": 1
+ },
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "label": "Title",
+ "reqd": 1
+ },
+ {
+ "fieldname": "parent_page",
+ "fieldtype": "Data",
+ "label": "Parent Page"
+ },
+ {
+ "default": "[]",
+ "fieldname": "content",
+ "fieldtype": "Long Text",
+ "hidden": 1,
+ "label": "Content"
+ },
+ {
+ "fieldname": "sequence_id",
+ "fieldtype": "Int",
+ "label": "Sequence Id"
+ },
+ {
+ "fieldname": "roles",
+ "fieldtype": "Table",
+ "label": "Roles",
+ "options": "Has Role"
+ },
+ {
+ "fieldname": "roles_section",
+ "fieldtype": "Section Break",
+ "label": "Roles"
+ }
],
+ "in_create": 1,
"links": [],
- "modified": "2021-01-21 12:09:36.156614",
+ "modified": "2021-09-16 12:01:06.450622",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -232,7 +173,7 @@
"print": 1,
"read": 1,
"report": 1,
- "role": "System Manager",
+ "role": "Workspace Manager",
"share": 1,
"write": 1
},
@@ -248,4 +189,4 @@
],
"sort_field": "modified",
"sort_order": "DESC"
-}
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py
index 0b5babc8d9..94114e3918 100644
--- a/frappe/desk/doctype/workspace/workspace.py
+++ b/frappe/desk/doctype/workspace/workspace.py
@@ -1,61 +1,56 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
from frappe.modules.export_file import export_to_files
from frappe.model.document import Document
+from frappe.desk.desktop import save_new_widget
from frappe.desk.utils import validate_route_conflict
from json import loads
class Workspace(Document):
def validate(self):
- if (self.is_standard and not frappe.conf.developer_mode and not disable_saving_as_standard()):
- frappe.throw(_("You need to be in developer mode to edit this document"))
+ if (self.public and not is_workspace_manager() and not disable_saving_as_public()):
+ frappe.throw(_("You need to be Workspace Manager to edit this document"))
validate_route_conflict(self.doctype, self.name)
- duplicate_exists = frappe.db.exists("Workspace", {
- "name": ["!=", self.name], 'is_default': 1, 'extends': self.extends
- })
-
- if self.is_default and self.name and duplicate_exists:
- frappe.throw(_("You can only have one default page that extends a particular standard page."))
+ try:
+ if not isinstance(loads(self.content), list):
+ raise
+ except Exception:
+ frappe.throw(_("Content data shoud be a list"))
def on_update(self):
- if disable_saving_as_standard():
+ if disable_saving_as_public():
return
- if frappe.conf.developer_mode and self.is_standard:
+ if frappe.conf.developer_mode and self.module and self.public:
export_to_files(record_list=[['Workspace', self.name]], record_module=self.module)
@staticmethod
def get_module_page_map():
- filters = {
- 'extends_another_page': 0,
- 'for_user': '',
- }
-
- pages = frappe.get_all("Workspace", fields=["name", "module"], filters=filters, as_list=1)
+ pages = frappe.get_all("Workspace", fields=["name", "module"], filters={'for_user': ''}, as_list=1)
return { page[1]: page[0] for page in pages if page[1] }
def get_link_groups(self):
cards = []
- current_card = {
+ current_card = frappe._dict({
"label": "Link",
"type": "Card Break",
"icon": None,
"hidden": False,
- }
+ })
card_links = []
for link in self.links:
link = link.as_dict()
if link.type == "Card Break":
- if card_links and (not current_card.only_for or current_card.only_for == frappe.get_system_settings('country')):
+ if card_links and (not current_card.get('only_for') or current_card.get('only_for') == frappe.get_system_settings('country')):
current_card['links'] = card_links
cards.append(current_card)
@@ -69,21 +64,23 @@ class Workspace(Document):
return cards
- def build_links_table_from_cards(self, config):
- # Empty links table
- self.links = []
- order = config.get('order')
- widgets = config.get('widgets')
+ def build_links_table_from_card(self, config):
- for idx, name in enumerate(order):
- card = widgets[name].copy()
+ for idx, card in enumerate(config):
links = loads(card.get('links'))
+ # remove duplicate before adding
+ for idx, link in enumerate(self.links):
+ if link.label == card.get('label') and link.type == 'Card Break':
+ del self.links[idx : idx + link.link_count + 1]
+
self.append('links', {
"label": card.get('label'),
"type": "Card Break",
"icon": card.get('icon'),
- "hidden": card.get('hidden') or False
+ "hidden": card.get('hidden') or False,
+ "link_count": card.get('link_count'),
+ "idx": 1 if not self.links else self.links[-1].idx + 1
})
for link in links:
@@ -95,11 +92,11 @@ class Workspace(Document):
"onboard": link.get('onboard'),
"only_for": link.get('only_for'),
"dependencies": link.get('dependencies'),
- "is_query_report": link.get('is_query_report')
+ "is_query_report": link.get('is_query_report'),
+ "idx": self.links[-1].idx + 1
})
-
-def disable_saving_as_standard():
+def disable_saving_as_public():
return frappe.flags.in_install or \
frappe.flags.in_patch or \
frappe.flags.in_test or \
@@ -123,3 +120,87 @@ def get_link_type(key):
def get_report_type(report):
report_type = frappe.get_value("Report", report, "report_type")
return report_type in ["Query Report", "Script Report", "Custom Report"]
+
+
+@frappe.whitelist()
+def save_page(title, icon, parent, public, sb_public_items, sb_private_items, deleted_pages, new_widgets, blocks, save):
+ save = frappe.parse_json(save)
+ public = frappe.parse_json(public)
+ if save:
+ doc = frappe.new_doc('Workspace')
+ doc.title = title
+ doc.icon = icon
+ doc.content = blocks
+ doc.parent_page = parent
+
+ if public:
+ doc.label = title
+ doc.public = 1
+ else:
+ doc.label = title + "-" + frappe.session.user
+ doc.for_user = frappe.session.user
+ doc.save(ignore_permissions=True)
+ else:
+ if public:
+ filters = {
+ 'public': public,
+ 'label': title
+ }
+ else:
+ filters = {
+ 'for_user': frappe.session.user,
+ 'label': title + "-" + frappe.session.user
+ }
+ pages = frappe.get_list("Workspace", filters=filters)
+ if pages:
+ doc = frappe.get_doc("Workspace", pages[0])
+
+ doc.content = blocks
+ doc.save(ignore_permissions=True)
+
+ if loads(new_widgets):
+ save_new_widget(doc, title, blocks, new_widgets)
+
+ if loads(sb_public_items) or loads(sb_private_items):
+ sort_pages(loads(sb_public_items), loads(sb_private_items))
+
+ if loads(deleted_pages):
+ return delete_pages(loads(deleted_pages))
+
+ return {"name": title, "public": public, "label": doc.label}
+
+def delete_pages(deleted_pages):
+ for page in deleted_pages:
+ if page.get("public") and not is_workspace_manager():
+ return {"name": page.get("title"), "public": 1, "label": page.get("label")}
+
+ if frappe.db.exists("Workspace", page.get("name")):
+ frappe.get_doc("Workspace", page.get("name")).delete(ignore_permissions=True)
+
+ return {"name": "Home", "public": 1, "label": "Home"}
+
+def sort_pages(sb_public_items, sb_private_items):
+ wspace_public_pages = get_page_list(['name', 'title'], {'public': 1})
+ wspace_private_pages = get_page_list(['name', 'title'], {'for_user': frappe.session.user})
+
+ if sb_private_items:
+ sort_page(wspace_private_pages, sb_private_items)
+
+ if sb_public_items and is_workspace_manager():
+ sort_page(wspace_public_pages, sb_public_items)
+
+def sort_page(wspace_pages, pages):
+ for seq, d in enumerate(pages):
+ for page in wspace_pages:
+ if page.title == d.get('title'):
+ doc = frappe.get_doc('Workspace', page.name)
+ doc.sequence_id = seq + 1
+ doc.parent_page = d.get('parent_page') or ""
+ doc.save(ignore_permissions=True)
+ break
+
+def get_page_list(fields, filters):
+ return frappe.get_list("Workspace", fields=fields, filters=filters, order_by='sequence_id asc')
+
+def is_workspace_manager():
+ return "Workspace Manager" in frappe.get_roles()
diff --git a/frappe/desk/doctype/workspace_chart/workspace_chart.py b/frappe/desk/doctype/workspace_chart/workspace_chart.py
index 6ec7abfd3c..a3b66d99ab 100644
--- a/frappe/desk/doctype/workspace_chart/workspace_chart.py
+++ b/frappe/desk/doctype/workspace_chart/workspace_chart.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/workspace_link/workspace_link.json b/frappe/desk/doctype/workspace_link/workspace_link.json
index 53dadad83d..a7b217be9e 100644
--- a/frappe/desk/doctype/workspace_link/workspace_link.json
+++ b/frappe/desk/doctype/workspace_link/workspace_link.json
@@ -8,15 +8,16 @@
"type",
"label",
"icon",
- "only_for",
"hidden",
"link_details_section",
"link_type",
"link_to",
"column_break_7",
"dependencies",
+ "only_for",
"onboard",
- "is_query_report"
+ "is_query_report",
+ "link_count"
],
"fields": [
{
@@ -99,12 +100,19 @@
"fieldname": "is_query_report",
"fieldtype": "Check",
"label": "Is Query Report"
+ },
+ {
+ "depends_on": "eval:doc.type == \"Card Break\"",
+ "fieldname": "link_count",
+ "fieldtype": "Int",
+ "hidden": 1,
+ "label": "Link Count"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-05-13 13:10:18.128512",
+ "modified": "2021-06-01 11:23:28.990593",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace Link",
diff --git a/frappe/desk/doctype/workspace_link/workspace_link.py b/frappe/desk/doctype/workspace_link/workspace_link.py
index d6ccc5306a..72256ba490 100644
--- a/frappe/desk/doctype/workspace_link/workspace_link.py
+++ b/frappe/desk/doctype/workspace_link/workspace_link.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.py b/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.py
index 83b446e454..1dad4cca05 100644
--- a/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.py
+++ b/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/desk/form/__init__.py b/frappe/desk/form/__init__.py
index 0e57cb68c3..eb5ba62e5c 100644
--- a/frappe/desk/form/__init__.py
+++ b/frappe/desk/form/__init__.py
@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
diff --git a/frappe/desk/form/assign_to.py b/frappe/desk/form/assign_to.py
index 3eda291d1e..bf77170eeb 100644
--- a/frappe/desk/form/assign_to.py
+++ b/frappe/desk/form/assign_to.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
"""assign/unassign to ToDo"""
diff --git a/frappe/desk/form/document_follow.py b/frappe/desk/form/document_follow.py
index 7f65f76a58..14970092d0 100644
--- a/frappe/desk/form/document_follow.py
+++ b/frappe/desk/form/document_follow.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import frappe.utils
diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py
index ae48b7fc6b..4550fdf0e6 100644
--- a/frappe/desk/form/linked_with.py
+++ b/frappe/desk/form/linked_with.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import json
from collections import defaultdict
@@ -77,7 +77,7 @@ def get_submitted_linked_docs(doctype, name, docs=None, visited=None):
@frappe.whitelist()
-def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=[]):
+def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=None):
"""
Cancel all linked doctype, optionally ignore doctypes specified in a list.
@@ -85,6 +85,8 @@ def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=[]):
docs (json str) - It contains list of dictionaries of a linked documents.
ignore_doctypes_on_cancel_all (list) - List of doctypes to ignore while cancelling.
"""
+ if ignore_doctypes_on_cancel_all is None:
+ ignore_doctypes_on_cancel_all = []
docs = json.loads(docs)
if isinstance(ignore_doctypes_on_cancel_all, str):
@@ -96,7 +98,7 @@ def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=[]):
frappe.publish_progress(percent=i/len(docs) * 100, title=_("Cancelling documents"))
-def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=[]):
+def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=None):
"""
Validate a document to be submitted and non-exempted from auto-cancel.
@@ -109,7 +111,7 @@ def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=[]):
"""
#ignore doctype to cancel
- if docinfo.get("doctype") in ignore_doctypes_on_cancel_all:
+ if docinfo.get("doctype") in (ignore_doctypes_on_cancel_all or []):
return False
# skip non-submittable doctypes since they don't need to be cancelled
diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py
index a62bfd01d0..89e6598859 100644
--- a/frappe/desk/form/load.py
+++ b/frappe/desk/form/load.py
@@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
+from typing import Dict, List, Union
import frappe, json
import frappe.utils
import frappe.share
@@ -12,7 +13,7 @@ from frappe.desk.form.document_follow import is_document_followed
from frappe import _
from urllib.parse import quote
-@frappe.whitelist(allow_guest=True)
+@frappe.whitelist()
def getdoc(doctype, name, user=None):
"""
Loads a doclist for a given document. This method is called directly from the client.
@@ -51,7 +52,7 @@ def getdoc(doctype, name, user=None):
frappe.response.docs.append(doc)
-@frappe.whitelist(allow_guest=True)
+@frappe.whitelist()
def getdoctype(doctype, with_parent=False, cached_timestamp=None):
"""load doctype"""
@@ -105,9 +106,10 @@ def get_docinfo(doc=None, doctype=None, name=None):
"assignment_logs": get_comments(doc.doctype, doc.name, 'assignment'),
"permissions": get_doc_permissions(doc),
"shared": frappe.share.get_users(doc.doctype, doc.name),
- "info_logs": get_comments(doc.doctype, doc.name, 'Info'),
+ "info_logs": get_comments(doc.doctype, doc.name, comment_type=['Info', 'Edit', 'Label']),
"share_logs": get_comments(doc.doctype, doc.name, 'share'),
"like_logs": get_comments(doc.doctype, doc.name, 'Like'),
+ "workflow_logs": get_comments(doc.doctype, doc.name, comment_type="Workflow"),
"views": get_view_logs(doc.doctype, doc.name),
"energy_point_logs": get_point_logs(doc.doctype, doc.name),
"additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name),
@@ -138,10 +140,11 @@ def get_communications(doctype, name, start=0, limit=20):
return _get_communications(doctype, name, start, limit)
-def get_comments(doctype, name, comment_type='Comment'):
- comment_types = [comment_type]
+def get_comments(doctype: str, name: str, comment_type : Union[str, List[str]] = "Comment") -> List[frappe._dict]:
+ if isinstance(comment_type, list):
+ comment_types = comment_type
- if comment_type == 'share':
+ elif comment_type == 'share':
comment_types = ['Shared', 'Unshared']
elif comment_type == 'assignment':
@@ -150,15 +153,21 @@ def get_comments(doctype, name, comment_type='Comment'):
elif comment_type == 'attachment':
comment_types = ['Attachment', 'Attachment Removed']
- comments = frappe.get_all('Comment', fields = ['name', 'creation', 'content', 'owner', 'comment_type'], filters=dict(
- reference_doctype = doctype,
- reference_name = name,
- comment_type = ['in', comment_types]
- ))
+ else:
+ comment_types = [comment_type]
+
+ comments = frappe.get_all("Comment",
+ fields=["name", "creation", "content", "owner", "comment_type"],
+ filters={
+ "reference_doctype": doctype,
+ "reference_name": name,
+ "comment_type": ['in', comment_types],
+ }
+ )
# convert to markdown (legacy ?)
- if comment_type == 'Comment':
- for c in comments:
+ for c in comments:
+ if c.comment_type == "Comment":
c.content = frappe.utils.markdown(c.content)
return comments
diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py
index cf3606e785..b91dd3d481 100644
--- a/frappe/desk/form/meta.py
+++ b/frappe/desk/form/meta.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import io
import os
diff --git a/frappe/desk/form/save.py b/frappe/desk/form/save.py
index a7a4b829d8..b580e2c769 100644
--- a/frappe/desk/form/save.py
+++ b/frappe/desk/form/save.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe, json
from frappe.desk.form.load import run_onload
diff --git a/frappe/desk/form/test_form.py b/frappe/desk/form/test_form.py
index f3c4132777..86c3aba29a 100644
--- a/frappe/desk/form/test_form.py
+++ b/frappe/desk/form/test_form.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe, unittest
diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py
index bfceee6ea2..291767de10 100644
--- a/frappe/desk/form/utils.py
+++ b/frappe/desk/form/utils.py
@@ -1,11 +1,11 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe, json
import frappe.desk.form.meta
import frappe.desk.form.load
from frappe.desk.form.document_follow import follow_document
-from frappe.utils.file_manager import extract_images_from_html
+from frappe.core.doctype.file.file import extract_images_from_html
from frappe import _
@@ -16,44 +16,6 @@ def remove_attach():
file_name = frappe.form_dict.get('file_name')
frappe.delete_doc('File', fid)
-@frappe.whitelist()
-def validate_link():
- """validate link when updated by user"""
- import frappe
- import frappe.utils
-
- value, options, fetch = frappe.form_dict.get('value'), frappe.form_dict.get('options'), frappe.form_dict.get('fetch')
-
- # no options, don't validate
- if not options or options=='null' or options=='undefined':
- frappe.response['message'] = 'Ok'
- return
-
- valid_value = frappe.db.get_all(options, filters=dict(name=value), as_list=1, limit=1)
-
- if valid_value:
- valid_value = valid_value[0][0]
-
- # get fetch values
- if fetch:
- # escape with "`"
- fetch = ", ".join(("`{0}`".format(f.strip()) for f in fetch.split(",")))
- fetch_value = None
- try:
- fetch_value = frappe.db.sql("select %s from `tab%s` where name=%s"
- % (fetch, options, '%s'), (value,))[0]
- except Exception as e:
- error_message = str(e).split("Unknown column '")
- fieldname = None if len(error_message)<=1 else error_message[1].split("'")[0]
- frappe.msgprint(_("Wrong fieldname {0} in add_fetch configuration of custom client script").format(fieldname))
- frappe.errprint(frappe.get_traceback())
-
- if fetch_value:
- frappe.response['fetch_values'] = [frappe.utils.parse_val(c) for c in fetch_value]
-
- frappe.response['valid_value'] = valid_value
- frappe.response['message'] = 'Ok'
-
@frappe.whitelist()
def add_comment(reference_doctype, reference_name, content, comment_email, comment_by):
@@ -66,7 +28,8 @@ def add_comment(reference_doctype, reference_name, content, comment_email, comme
comment_type='Comment',
comment_by=comment_by
))
- doc.content = extract_images_from_html(doc, content)
+ reference_doc = frappe.get_doc(reference_doctype, reference_name)
+ doc.content = extract_images_from_html(reference_doc, content, is_private=True)
doc.insert(ignore_permissions=True)
follow_document(doc.reference_doctype, doc.reference_name, frappe.session.user)
diff --git a/frappe/desk/gantt.py b/frappe/desk/gantt.py
index 7f0889c751..58ef3b836e 100644
--- a/frappe/desk/gantt.py
+++ b/frappe/desk/gantt.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe, json
diff --git a/frappe/desk/like.py b/frappe/desk/like.py
index d44d58a761..4480ed8a1e 100644
--- a/frappe/desk/like.py
+++ b/frappe/desk/like.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
"""Allow adding of likes to documents"""
diff --git a/frappe/desk/link_preview.py b/frappe/desk/link_preview.py
index 9b4471aa8d..03f8368a3a 100644
--- a/frappe/desk/link_preview.py
+++ b/frappe/desk/link_preview.py
@@ -40,6 +40,10 @@ def get_preview_data(doctype, docname):
for key, val in preview_data.items():
if val and meta.has_field(key) and key not in [image_field, title_field, 'name']:
- formatted_preview_data[meta.get_field(key).label] = frappe.format(val, meta.get_field(key).fieldtype)
+ formatted_preview_data[meta.get_field(key).label] = frappe.format(
+ val,
+ meta.get_field(key).fieldtype,
+ translated=True,
+ )
return formatted_preview_data
diff --git a/frappe/desk/listview.py b/frappe/desk/listview.py
index d2c84d36bf..43ad104f0d 100644
--- a/frappe/desk/listview.py
+++ b/frappe/desk/listview.py
@@ -1,8 +1,8 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
-@frappe.whitelist(allow_guest=True)
+@frappe.whitelist()
def get_list_settings(doctype):
try:
return frappe.get_cached_doc("List View Settings", doctype)
@@ -26,7 +26,7 @@ def get_group_by_count(doctype, current_filters, field):
current_filters = frappe.parse_json(current_filters)
subquery_condition = ''
- subquery = frappe.get_all(doctype, filters=current_filters, return_query = True)
+ subquery = frappe.get_all(doctype, filters=current_filters, run=False)
if field == 'assigned_to':
subquery_condition = ' and `tabToDo`.reference_name in ({subquery})'.format(subquery = subquery)
return frappe.db.sql("""select `tabToDo`.owner as name, count(*) as count
diff --git a/frappe/desk/moduleview.py b/frappe/desk/moduleview.py
index 021698ac92..e2e2c4c155 100644
--- a/frappe/desk/moduleview.py
+++ b/frappe/desk/moduleview.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import json
diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py
index c84027928e..3fa41790b4 100644
--- a/frappe/desk/notifications.py
+++ b/frappe/desk/notifications.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.desk.doctype.notification_settings.notification_settings import get_subscribed_documents
@@ -216,7 +216,7 @@ def get_filters_for(doctype):
@frappe.whitelist()
@frappe.read_only()
-def get_open_count(doctype, name, items=[]):
+def get_open_count(doctype, name, items=None):
'''Get open count for given transactions and filters
:param doctype: Reference DocType
@@ -235,7 +235,8 @@ def get_open_count(doctype, name, items=[]):
links = meta.get_dashboard_data()
# compile all items in a list
- if not items:
+ if items is None:
+ items = []
for group in links.transactions:
items.extend(group.get("items"))
diff --git a/frappe/desk/page/activity/activity.py b/frappe/desk/page/activity/activity.py
index 3abc8e0ea5..71130f2304 100644
--- a/frappe/desk/page/activity/activity.py
+++ b/frappe/desk/page/activity/activity.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.utils import cint
diff --git a/frappe/desk/page/backups/backups.html b/frappe/desk/page/backups/backups.html
index e63481487c..ff10f1bd06 100644
--- a/frappe/desk/page/backups/backups.html
+++ b/frappe/desk/page/backups/backups.html
@@ -1,20 +1,27 @@
- {% for f in files %}
-
- {% endfor %}
+ {% for f in files %}
+
+ {% endfor %}
\ No newline at end of file
diff --git a/frappe/desk/page/backups/backups.js b/frappe/desk/page/backups/backups.js
index 337ad33f43..d6cab750f0 100644
--- a/frappe/desk/page/backups/backups.js
+++ b/frappe/desk/page/backups/backups.js
@@ -1,4 +1,4 @@
-frappe.pages['backups'].on_page_load = function(wrapper) {
+frappe.pages['backups'].on_page_load = function (wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: __('Download Backups'),
@@ -11,12 +11,35 @@ frappe.pages['backups'].on_page_load = function(wrapper) {
page.add_inner_button(__("Download Files Backup"), function () {
frappe.call({
- method:"frappe.desk.page.backups.backups.schedule_files_backup",
- args: {"user_email": frappe.session.user_email}
+ method: "frappe.desk.page.backups.backups.schedule_files_backup",
+ args: { "user_email": frappe.session.user_email }
});
});
+ page.add_inner_button(__("Get Backup Encryption Key"), function () {
+ if (frappe.user.has_role("System Manager")) {
+ frappe.verify_password(function () {
+ frappe.call({
+ method: "frappe.utils.backups.get_backup_encryption_key",
+ callback: function (r) {
+ frappe.msgprint({
+ title: __('Backup Encryption Key'),
+ message: __(r.message),
+ indicator: 'blue'
+ });
+ }
+ });
+ });
+ } else {
+ frappe.msgprint({
+ title: __('Error'),
+ message: __('System Manager privileges required.'),
+ indicator: 'red'
+ });
+ }
+ });
+
frappe.breadcrumbs.add("Setup");
$(frappe.render_template("backups")).appendTo(page.body.addClass("no-border"));
-}
+};
diff --git a/frappe/desk/page/backups/backups.py b/frappe/desk/page/backups/backups.py
index 2229a6d89e..14ed025e08 100644
--- a/frappe/desk/page/backups/backups.py
+++ b/frappe/desk/page/backups/backups.py
@@ -11,6 +11,10 @@ def get_context(context):
dt = os.path.getmtime(path)
return convert_utc_to_user_timezone(datetime.datetime.utcfromtimestamp(dt)).strftime('%a %b %d %H:%M %Y')
+ def get_encrytion_status(path):
+ if "-enc" in path:
+ return True
+
def get_size(path):
size = os.path.getsize(path)
if size > 1048576:
@@ -26,8 +30,9 @@ def get_context(context):
cleanup_old_backups(path, files, backup_limit)
files = [('/backups/' + _file,
- get_time(os.path.join(path, _file)),
- get_size(os.path.join(path, _file))) for _file in files if _file.endswith('sql.gz')]
+ get_time(os.path.join(path, _file)),
+ get_encrytion_status(os.path.join(path, _file)),
+ get_size(os.path.join(path, _file))) for _file in files if _file.endswith('sql.gz')]
files.sort(key=lambda x: x[1], reverse=True)
return {"files": files[:backup_limit]}
diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js
index b3fccf84f9..076d672db5 100644
--- a/frappe/desk/page/leaderboard/leaderboard.js
+++ b/frappe/desk/page/leaderboard/leaderboard.js
@@ -141,7 +141,7 @@ class Leaderboard {
}
create_date_range_field() {
- let timespan_field = $(this.parent).find(`.frappe-control[data-original-title=${__('Timespan')}]`);
+ let timespan_field = $(this.parent).find(`.frappe-control[data-original-title="${__('Timespan')}"]`);
this.date_range_field = $(``).insertAfter(timespan_field).hide();
let date_field = frappe.ui.form.make_control({
diff --git a/frappe/desk/page/leaderboard/leaderboard.py b/frappe/desk/page/leaderboard/leaderboard.py
index 9469096f50..ad22eb9199 100644
--- a/frappe/desk/page/leaderboard/leaderboard.py
+++ b/frappe/desk/page/leaderboard/leaderboard.py
@@ -1,5 +1,5 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
@frappe.whitelist()
diff --git a/frappe/desk/page/setup_wizard/install_fixtures.py b/frappe/desk/page/setup_wizard/install_fixtures.py
index 06301cdeaf..1ef83f7ba0 100644
--- a/frappe/desk/page/setup_wizard/install_fixtures.py
+++ b/frappe/desk/page/setup_wizard/install_fixtures.py
@@ -1,5 +1,5 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# License: MIT. See LICENSE
import frappe
from frappe import _
diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py
index 5edb44e182..c729c1d78b 100755
--- a/frappe/desk/page/setup_wizard/setup_wizard.py
+++ b/frappe/desk/page/setup_wizard/setup_wizard.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: See license.txt
+# License: MIT. See LICENSE
import frappe, json, os
from frappe.utils import strip, cint
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index 3c0ebf11c1..97bceeb725 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import os
@@ -59,6 +59,20 @@ def get_report_doc(report_name):
return doc
+def get_report_result(report, filters):
+ if report.report_type == "Query Report":
+ res = report.execute_query_report(filters)
+
+ elif report.report_type == "Script Report":
+ res = report.execute_script_report(filters)
+
+ elif report.report_type == "Custom Report":
+ ref_report = get_report_doc(report.report_name)
+ res = get_report_result(ref_report, filters)
+
+ return res
+
+@frappe.read_only()
def generate_report_result(report, filters=None, user=None, custom_columns=None):
user = user or frappe.session.user
filters = filters or []
@@ -66,13 +80,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
if filters and isinstance(filters, str):
filters = json.loads(filters)
- res = []
-
- if report.report_type == "Query Report":
- res = report.execute_query_report(filters)
-
- elif report.report_type == "Script Report":
- res = report.execute_script_report(filters)
+ res = get_report_result(report, filters) or []
columns, result, message, chart, report_summary, skip_total_row = ljust_list(res, 6)
columns = [get_column_as_dict(col) for col in columns]
@@ -177,11 +185,13 @@ def get_script(report_name):
if os.path.exists(script_path):
with open(script_path, "r") as f:
script = f.read()
+ script += f"\n\n//# sourceURL={scrub(report.name)}.js"
html_format = get_html_format(print_path)
if not script and report.javascript:
script = report.javascript
+ script += f"\n\n//# sourceURL={scrub(report.name)}__custom"
if not script:
script = "frappe.query_reports['%s']={}" % report_name
@@ -389,14 +399,14 @@ def handle_duration_fieldtype_values(result, columns):
return result
-def build_xlsx_data(columns, data, visible_idx, include_indentation):
+def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visible_idx=False):
result = [[]]
column_widths = []
for column in data.columns:
if column.get("hidden"):
continue
- result[0].append(column["label"])
+ result[0].append(column.get("label"))
column_width = cint(column.get('width', 0))
# to convert into scale accepted by openpyxl
column_width /= 10
@@ -405,7 +415,7 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation):
# build table from result
for row_idx, row in enumerate(data.result):
# only pick up rows that are visible in the report
- if row_idx in visible_idx:
+ if ignore_visible_idx or row_idx in visible_idx:
row_data = []
if isinstance(row, dict):
for col_idx, column in enumerate(data.columns):
diff --git a/frappe/desk/report/todo/todo.py b/frappe/desk/report/todo/todo.py
index 6bd22b843e..b1e49bc95d 100644
--- a/frappe/desk/report/todo/todo.py
+++ b/frappe/desk/report/todo/todo.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
diff --git a/frappe/desk/report_dump.py b/frappe/desk/report_dump.py
index b2d3ca3443..f57ed97fa5 100644
--- a/frappe/desk/report_dump.py
+++ b/frappe/desk/report_dump.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py
index 55515856f1..fb150e4bea 100644
--- a/frappe/desk/reportview.py
+++ b/frappe/desk/reportview.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
"""build query for doclistview and return results"""
@@ -14,7 +14,7 @@ from frappe.utils import cstr, format_duration
from frappe.model.base_document import get_controller
-@frappe.whitelist(allow_guest=True)
+@frappe.whitelist()
@frappe.read_only()
def get():
args = get_form_params()
@@ -121,12 +121,14 @@ def validate_filters(data, filters):
def setup_group_by(data):
'''Add columns for aggregated values e.g. count(name)'''
- if data.group_by:
+ if data.group_by and data.aggregate_function:
if data.aggregate_function.lower() not in ('count', 'sum', 'avg'):
frappe.throw(_('Invalid aggregate function'))
if frappe.db.has_column(data.aggregate_on_doctype, data.aggregate_on_field):
data.fields.append('{aggregate_function}(`tab{aggregate_on_doctype}`.`{aggregate_on_field}`) AS _aggregate_column'.format(**data))
+ if data.aggregate_on_field:
+ data.fields.append(f"`tab{data.aggregate_on_doctype}`.`{data.aggregate_on_field}`")
else:
raise_invalid_field(data.aggregate_on_field)
@@ -178,15 +180,16 @@ def update_wildcard_field_param(data):
def clean_params(data):
- data.pop('cmd', None)
- data.pop('data', None)
- data.pop('ignore_permissions', None)
- data.pop('view', None)
- data.pop('user', None)
-
- if "csrf_token" in data:
- del data["csrf_token"]
-
+ for param in (
+ "cmd",
+ "data",
+ "ignore_permissions",
+ "view",
+ "user",
+ "csrf_token",
+ "join"
+ ):
+ data.pop(param, None)
def parse_json(data):
if isinstance(data.get("filters"), str):
@@ -212,11 +215,13 @@ def get_parenttype_and_fieldname(field, data):
return parenttype, fieldname
-def compress(data, args = {}):
+def compress(data, args=None):
"""separate keys and values"""
from frappe.desk.query_report import add_total_row
if not data: return data
+ if args is None:
+ args = {}
values = []
keys = list(data[0])
for row in data:
@@ -421,15 +426,20 @@ def delete_bulk(doctype, items):
@frappe.whitelist()
@frappe.read_only()
-def get_sidebar_stats(stats, doctype, filters=[]):
+def get_sidebar_stats(stats, doctype, filters=None):
+ if filters is None:
+ filters = []
return {"stats": get_stats(stats, doctype, filters)}
@frappe.whitelist()
@frappe.read_only()
-def get_stats(stats, doctype, filters=[]):
+def get_stats(stats, doctype, filters=None):
"""get tag info"""
import json
+
+ if filters is None:
+ filters = []
tags = json.loads(stats)
if filters:
filters = json.loads(filters)
@@ -445,33 +455,44 @@ def get_stats(stats, doctype, filters=[]):
for tag in tags:
if not tag in columns: continue
try:
- tagcount = frappe.get_list(doctype, fields=[tag, "count(*)"],
- #filters=["ifnull(`%s`,'')!=''" % tag], group_by=tag, as_list=True)
- filters = filters + ["ifnull(`%s`,'')!=''" % tag], group_by = tag, as_list = True)
+ tag_count = frappe.get_list(doctype,
+ fields=[tag, "count(*)"],
+ filters=filters + [[tag, '!=', '']],
+ group_by=tag,
+ as_list=True,
+ distinct=1,
+ )
- if tag=='_user_tags':
- stats[tag] = scrub_user_tags(tagcount)
- stats[tag].append([_("No Tags"), frappe.get_list(doctype,
+ if tag == '_user_tags':
+ stats[tag] = scrub_user_tags(tag_count)
+ no_tag_count = frappe.get_list(doctype,
fields=[tag, "count(*)"],
- filters=filters +["({0} = ',' or {0} = '' or {0} is null)".format(tag)], as_list=True)[0][1]])
+ filters=filters + [[tag, "in", ('', ',')]],
+ as_list=True,
+ group_by=tag,
+ order_by=tag,
+ )
+
+ no_tag_count = no_tag_count[0][1] if no_tag_count else 0
+
+ stats[tag].append([_("No Tags"), no_tag_count])
else:
- stats[tag] = tagcount
+ stats[tag] = tag_count
except frappe.db.SQLError:
- # does not work for child tables
pass
- except frappe.db.InternalError:
+ except frappe.db.InternalError as e:
# raised when _user_tags column is added on the fly
pass
+
return stats
@frappe.whitelist()
-def get_filter_dashboard_data(stats, doctype, filters=[]):
+def get_filter_dashboard_data(stats, doctype, filters=None):
"""get tags info"""
import json
tags = json.loads(stats)
- if filters:
- filters = json.loads(filters)
+ filters = json.loads(filters or [])
stats = {}
columns = frappe.db.get_table_columns(doctype)
diff --git a/frappe/desk/search.py b/frappe/desk/search.py
index 040a8c2118..db88e6ec52 100644
--- a/frappe/desk/search.py
+++ b/frappe/desk/search.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
# Search
import frappe, json
@@ -168,7 +168,18 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
strict=False)
if doctype in UNTRANSLATED_DOCTYPES:
- values = tuple([v for v in list(values) if re.search(re.escape(txt)+".*", (_(v.name) if as_dict else _(v[0])), re.IGNORECASE)])
+ # Filtering the values array so that query is included in very element
+ values = (
+ v for v in values
+ if re.search(
+ f"{re.escape(txt)}.*", _(v.name if as_dict else v[0]), re.IGNORECASE
+ )
+ )
+
+ # Sorting the values array so that relevant results always come first
+ # This will first bring elements on top in which query is a prefix of element
+ # Then it will bring the rest of the elements and sort them in lexicographical order
+ values = sorted(values, key=lambda x: relevance_sorter(x, txt, as_dict))
# remove _relevance from results
if as_dict:
@@ -208,6 +219,13 @@ def scrub_custom_query(query, key, txt):
query = query.replace('%s', ((txt or '') + '%'))
return query
+def relevance_sorter(key, query, as_dict):
+ value = _(key.name if as_dict else key[0])
+ return (
+ value.lower().startswith(query.lower()) is not True,
+ value
+ )
+
@wrapt.decorator
def validate_and_sanitize_search_inputs(fn, instance, args, kwargs):
kwargs.update(dict(zip(fn.__code__.co_varnames, args)))
@@ -247,6 +265,7 @@ def get_users_for_mentions():
'name': ['not in', ('Administrator', 'Guest')],
'allowed_in_mentions': True,
'user_type': 'System User',
+ 'enabled': True,
})
def get_user_groups():
diff --git a/frappe/desk/treeview.py b/frappe/desk/treeview.py
index 66acde4cb2..f40c135653 100644
--- a/frappe/desk/treeview.py
+++ b/frappe/desk/treeview.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
@@ -69,13 +69,11 @@ def make_tree_args(**kwarg):
doctype = kwarg['doctype']
parent_field = 'parent_' + doctype.lower().replace(' ', '_')
- name_field = kwarg.get('name_field', doctype.lower().replace(' ', '_') + '_name')
if kwarg['is_root'] == 'false': kwarg['is_root'] = False
if kwarg['is_root'] == 'true': kwarg['is_root'] = True
kwarg.update({
- name_field: kwarg[name_field],
parent_field: kwarg.get("parent") or kwarg.get(parent_field)
})
diff --git a/frappe/desk/utils.py b/frappe/desk/utils.py
index 01b47ac106..5908277386 100644
--- a/frappe/desk/utils.py
+++ b/frappe/desk/utils.py
@@ -1,5 +1,5 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/email/__init__.py b/frappe/email/__init__.py
index 3fb539398a..79dec977b7 100644
--- a/frappe/email/__init__.py
+++ b/frappe/email/__init__.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.desk.reportview import build_match_conditions
diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py
index f30279e308..34728375cd 100644
--- a/frappe/email/doctype/auto_email_report/auto_email_report.py
+++ b/frappe/email/doctype/auto_email_report/auto_email_report.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import calendar
from datetime import timedelta
@@ -13,6 +13,7 @@ from frappe.utils import (format_time, get_link_to_form, get_url_to_report,
from frappe.model.naming import append_number_if_name_exists
from frappe.utils.csvutils import to_csv
from frappe.utils.xlsxutils import make_xlsx
+from frappe.desk.query_report import build_xlsx_data
max_reports_per_user = frappe.local.conf.max_reports_per_user or 3
@@ -99,13 +100,21 @@ class AutoEmailReport(Document):
return self.get_html_table(columns, data)
elif self.format == 'XLSX':
- spreadsheet_data = self.get_spreadsheet_data(columns, data)
- xlsx_file = make_xlsx(spreadsheet_data, "Auto Email Report")
+ report_data = frappe._dict()
+ report_data['columns'] = columns
+ report_data['result'] = data
+
+ xlsx_data, column_widths = build_xlsx_data(columns, report_data, [], 1, ignore_visible_idx=True)
+ xlsx_file = make_xlsx(xlsx_data, "Auto Email Report", column_widths=column_widths)
return xlsx_file.getvalue()
elif self.format == 'CSV':
- spreadsheet_data = self.get_spreadsheet_data(columns, data)
- return to_csv(spreadsheet_data)
+ report_data = frappe._dict()
+ report_data['columns'] = columns
+ report_data['result'] = data
+
+ xlsx_data, column_widths = build_xlsx_data(columns, report_data, [], 1, ignore_visible_idx=True)
+ return to_csv(xlsx_data)
else:
frappe.throw(_('Invalid Output Format'))
@@ -126,18 +135,6 @@ class AutoEmailReport(Document):
'edit_report_settings': get_link_to_form('Auto Email Report', self.name)
})
- @staticmethod
- def get_spreadsheet_data(columns, data):
- out = [[_(df.label) for df in columns], ]
- for row in data:
- new_row = []
- out.append(new_row)
- for df in columns:
- if df.fieldname not in row: continue
- new_row.append(frappe.format(row[df.fieldname], df, row))
-
- return out
-
def get_file_name(self):
return "{0}.{1}".format(self.report.replace(" ", "-").replace("/", "-"), self.format.lower())
@@ -245,14 +242,17 @@ def make_links(columns, data):
for row in data:
doc_name = row.get('name')
for col in columns:
- if col.fieldtype == "Link" and col.options != "Currency":
- if col.options and row.get(col.fieldname):
+ if not row.get(col.fieldname):
+ continue
+
+ if col.fieldtype == "Link":
+ if col.options and col.options != "Currency":
row[col.fieldname] = get_link_to_form(col.options, row[col.fieldname])
elif col.fieldtype == "Dynamic Link":
- if col.options and row.get(col.fieldname) and row.get(col.options):
+ if col.options and row.get(col.options):
row[col.fieldname] = get_link_to_form(row[col.options], row[col.fieldname])
- elif col.fieldtype == "Currency" and row.get(col.fieldname):
- doc = frappe.get_doc(col.parent, doc_name) if doc_name else None
+ elif col.fieldtype == "Currency":
+ doc = frappe.get_doc(col.parent, doc_name) if doc_name and col.parent else None
# Pass the Document to get the currency based on docfield option
row[col.fieldname] = frappe.format_value(row[col.fieldname], col, doc=doc)
return columns, data
diff --git a/frappe/email/doctype/auto_email_report/test_auto_email_report.py b/frappe/email/doctype/auto_email_report/test_auto_email_report.py
index 211a141ec0..559adfbe1a 100644
--- a/frappe/email/doctype/auto_email_report/test_auto_email_report.py
+++ b/frappe/email/doctype/auto_email_report/test_auto_email_report.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import json
import unittest
diff --git a/frappe/email/doctype/document_follow/document_follow.py b/frappe/email/doctype/document_follow/document_follow.py
index a04f8ef4c2..97f8237736 100644
--- a/frappe/email/doctype/document_follow/document_follow.py
+++ b/frappe/email/doctype/document_follow/document_follow.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from frappe.model.document import Document
diff --git a/frappe/email/doctype/document_follow/test_document_follow.py b/frappe/email/doctype/document_follow/test_document_follow.py
index 456c0931f8..050add65e9 100644
--- a/frappe/email/doctype/document_follow/test_document_follow.py
+++ b/frappe/email/doctype/document_follow/test_document_follow.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
import frappe.desk.form.document_follow as document_follow
diff --git a/frappe/email/doctype/email_account/email_account.js b/frappe/email/doctype/email_account/email_account.js
index 83896e0af7..277bf43eb6 100644
--- a/frappe/email/doctype/email_account/email_account.js
+++ b/frappe/email/doctype/email_account/email_account.js
@@ -151,18 +151,6 @@ frappe.ui.form.on("Email Account", {
callback: function (r) {
if (r.message) {
frm.events.set_domain_fields(frm, r.message);
- } else {
- frm.set_value("domain", "");
- frappe.confirm(__('Email Domain not configured for this account, Create one?'),
- function () {
- frappe.model.with_doctype("Email Domain", function() {
- frappe.route_options = { email_id: frm.doc.email_id };
- frappe.route_flags.return_to_email_account = 1;
- var doc = frappe.model.get_new_doc("Email Domain");
- frappe.set_route("Form", "Email Domain", doc.name);
- });
- }
- );
}
}
});
diff --git a/frappe/email/doctype/email_account/email_account.json b/frappe/email/doctype/email_account/email_account.json
index 6d811b801f..e20f38c74a 100644
--- a/frappe/email/doctype/email_account/email_account.json
+++ b/frappe/email/doctype/email_account/email_account.json
@@ -7,30 +7,34 @@
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
+ "account_section",
"email_id",
- "login_id_is_different",
- "login_id",
+ "email_account_name",
+ "column_break_3",
+ "domain",
+ "service",
+ "authentication_column",
"password",
"awaiting_password",
"ascii_encode_password",
- "email_account_name",
- "email_settings",
- "domain",
- "service",
+ "column_break_10",
+ "login_id_is_different",
+ "login_id",
"mailbox_settings",
"enable_incoming",
- "use_imap",
- "email_server",
- "use_ssl",
- "append_emails_to_sent_folder",
- "incoming_port",
- "attachment_limit",
- "append_to",
"default_incoming",
+ "use_imap",
+ "use_ssl",
+ "email_server",
+ "incoming_port",
+ "column_break_18",
+ "attachment_limit",
"email_sync_option",
"initial_sync_count",
- "create_contact",
"section_break_12",
+ "append_emails_to_sent_folder",
+ "append_to",
+ "create_contact",
"enable_automatic_linking",
"section_break_13",
"notify_if_unreplied",
@@ -42,6 +46,7 @@
"use_tls",
"use_ssl_for_outgoing",
"smtp_port",
+ "column_break_38",
"default_outgoing",
"always_use_account_email_id_as_sender",
"always_use_account_name_as_sender_name",
@@ -80,7 +85,7 @@
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
- "label": "Use Different Email Login ID"
+ "label": "Use different login"
},
{
"depends_on": "login_id_is_different",
@@ -122,12 +127,6 @@
"label": "Email Account Name",
"unique": 1
},
- {
- "fieldname": "email_settings",
- "fieldtype": "Section Break",
- "hide_days": 1,
- "hide_seconds": 1
- },
{
"depends_on": "eval:!doc.service",
"fieldname": "domain",
@@ -136,7 +135,7 @@
"hide_seconds": 1,
"in_list_view": 1,
"in_standard_filter": 1,
- "label": "Domain",
+ "label": "Domain (optional)",
"options": "Email Domain"
},
{
@@ -145,18 +144,18 @@
"fieldtype": "Select",
"hide_days": 1,
"hide_seconds": 1,
- "label": "Service",
+ "label": "Service (optional)",
"options": "\nGMail\nSendgrid\nSparkPost\nYahoo Mail\nOutlook.com\nYandex.Mail"
},
{
"fieldname": "mailbox_settings",
"fieldtype": "Section Break",
"hide_days": 1,
- "hide_seconds": 1
+ "hide_seconds": 1,
+ "label": "Incoming (POP/IMAP) Settings"
},
{
"default": "0",
- "description": "Check this to pull emails from your mailbox",
"fieldname": "enable_incoming",
"fieldtype": "Check",
"hide_days": 1,
@@ -227,7 +226,7 @@
},
{
"default": "UNSEEN",
- "depends_on": "eval: doc.enable_incoming",
+ "depends_on": "eval: doc.enable_incoming && doc.use_imap",
"fieldname": "email_sync_option",
"fieldtype": "Select",
"hide_days": 1,
@@ -237,6 +236,7 @@
},
{
"default": "250",
+ "depends_on": "eval: doc.enable_incoming && doc.use_imap",
"description": "Total number of emails to sync in initial sync process ",
"fieldname": "initial_sync_count",
"fieldtype": "Select",
@@ -248,7 +248,7 @@
{
"depends_on": "enable_incoming",
"fieldname": "section_break_13",
- "fieldtype": "Section Break",
+ "fieldtype": "Column Break",
"hide_days": 1,
"hide_seconds": 1
},
@@ -282,7 +282,8 @@
"fieldname": "outgoing_mail_settings",
"fieldtype": "Section Break",
"hide_days": 1,
- "hide_seconds": 1
+ "hide_seconds": 1,
+ "label": "Outgoing (SMTP) Settings"
},
{
"default": "0",
@@ -336,22 +337,20 @@
{
"default": "0",
"depends_on": "enable_outgoing",
- "description": "Uses the Email Address mentioned in this Account as the Sender for all emails sent using this Account. ",
"fieldname": "always_use_account_email_id_as_sender",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
- "label": "Always use Account's Email Address as Sender"
+ "label": "Always use this email address as sender address"
},
{
"default": "0",
"depends_on": "enable_outgoing",
- "description": "Uses the Email Address Name mentioned in this Account as the Sender's Name for all emails sent using this Account.",
"fieldname": "always_use_account_name_as_sender_name",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
- "label": "Always use Account's Name as Sender's Name"
+ "label": "Always use this name as sender name"
},
{
"default": "1",
@@ -379,10 +378,13 @@
"label": "Disable SMTP server authentication"
},
{
+ "collapsible": 1,
+ "collapsible_depends_on": "add_signature",
"fieldname": "signature_section",
"fieldtype": "Section Break",
"hide_days": 1,
- "hide_seconds": 1
+ "hide_seconds": 1,
+ "label": "Signature"
},
{
"default": "0",
@@ -401,10 +403,13 @@
"label": "Signature"
},
{
+ "collapsible": 1,
+ "collapsible_depends_on": "enable_auto_reply",
"fieldname": "auto_reply",
"fieldtype": "Section Break",
"hide_days": 1,
- "hide_seconds": 1
+ "hide_seconds": 1,
+ "label": "Auto Reply"
},
{
"default": "0",
@@ -424,17 +429,20 @@
"label": "Auto Reply Message"
},
{
+ "collapsible": 1,
+ "collapsible_depends_on": "eval:frappe.utils.html2text(doc.footer || '')!=''",
"fieldname": "set_footer",
"fieldtype": "Section Break",
"hide_days": 1,
- "hide_seconds": 1
+ "hide_seconds": 1,
+ "label": "Footer"
},
{
"fieldname": "footer",
"fieldtype": "Text Editor",
"hide_days": 1,
"hide_seconds": 1,
- "label": "Footer"
+ "label": "Footer Content"
},
{
"fieldname": "uidvalidity",
@@ -477,7 +485,8 @@
"fieldname": "section_break_12",
"fieldtype": "Section Break",
"hide_days": 1,
- "hide_seconds": 1
+ "hide_seconds": 1,
+ "label": "Document Linking"
},
{
"default": "0",
@@ -527,12 +536,38 @@
"fieldname": "brand_logo",
"fieldtype": "Attach Image",
"label": "Brand Logo"
+ },
+ {
+ "fieldname": "authentication_column",
+ "fieldtype": "Section Break",
+ "label": "Authentication"
+ },
+ {
+ "fieldname": "column_break_10",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_18",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_38",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "account_section",
+ "fieldtype": "Section Break",
+ "label": "Account"
}
],
"icon": "fa fa-inbox",
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-01-21 10:05:24.820597",
+ "modified": "2021-09-21 16:44:25.728637",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Account",
@@ -554,4 +589,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index ecd59f42bb..d90c56d90d 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import email.utils
import functools
import imaplib
@@ -137,8 +137,6 @@ class EmailAccount(Document):
def on_update(self):
"""Check there is only one default of each type."""
- from frappe.core.doctype.user.user import setup_user_email_inbox
-
self.check_automatic_linking_email_account()
self.there_must_be_only_one_default()
setup_user_email_inbox(email_account=self.name, awaiting_password=self.awaiting_password,
@@ -532,8 +530,6 @@ class EmailAccount(Document):
def on_trash(self):
"""Clear communications where email account is linked"""
- from frappe.core.doctype.user.user import remove_user_email_inbox
-
frappe.db.sql("update `tabCommunication` set email_account='' where email_account=%s", self.name)
remove_user_email_inbox(email_account=self.name)
@@ -724,3 +720,84 @@ def get_max_email_uid(email_account):
else:
max_uid = cint(result[0].get("uid", 0)) + 1
return max_uid
+
+
+def setup_user_email_inbox(email_account, awaiting_password, email_id, enable_outgoing):
+ """ setup email inbox for user """
+ from frappe.core.doctype.user.user import ask_pass_update
+
+ def add_user_email(user):
+ user = frappe.get_doc("User", user)
+ row = user.append("user_emails", {})
+
+ row.email_id = email_id
+ row.email_account = email_account
+ row.awaiting_password = awaiting_password or 0
+ row.enable_outgoing = enable_outgoing or 0
+
+ user.save(ignore_permissions=True)
+
+ update_user_email_settings = False
+ if not all([email_account, email_id]):
+ return
+
+ user_names = frappe.db.get_values("User", {"email": email_id}, as_dict=True)
+ if not user_names:
+ return
+
+ for user in user_names:
+ user_name = user.get("name")
+
+ # check if inbox is alreay configured
+ user_inbox = frappe.db.get_value("User Email", {
+ "email_account": email_account,
+ "parent": user_name
+ }, ["name"]) or None
+
+ if not user_inbox:
+ add_user_email(user_name)
+ else:
+ # update awaiting password for email account
+ update_user_email_settings = True
+
+ if update_user_email_settings:
+ frappe.db.sql("""UPDATE `tabUser Email` SET awaiting_password = %(awaiting_password)s,
+ enable_outgoing = %(enable_outgoing)s WHERE email_account = %(email_account)s""", {
+ "email_account": email_account,
+ "enable_outgoing": enable_outgoing,
+ "awaiting_password": awaiting_password or 0
+ })
+ else:
+ users = " and ".join([frappe.bold(user.get("name")) for user in user_names])
+ frappe.msgprint(_("Enabled email inbox for user {0}").format(users))
+ ask_pass_update()
+
+def remove_user_email_inbox(email_account):
+ """ remove user email inbox settings if email account is deleted """
+ if not email_account:
+ return
+
+ users = frappe.get_all("User Email", filters={
+ "email_account": email_account
+ }, fields=["parent as name"])
+
+ for user in users:
+ doc = frappe.get_doc("User", user.get("name"))
+ to_remove = [row for row in doc.user_emails if row.email_account == email_account]
+ [doc.remove(row) for row in to_remove]
+
+ doc.save(ignore_permissions=True)
+
+@frappe.whitelist(allow_guest=False)
+def set_email_password(email_account, user, password):
+ account = frappe.get_doc("Email Account", email_account)
+ if account.awaiting_password:
+ account.awaiting_password = 0
+ account.password = password
+ try:
+ account.save(ignore_permissions=True)
+ except Exception:
+ frappe.db.rollback()
+ return False
+
+ return True
\ No newline at end of file
diff --git a/frappe/email/doctype/email_account/test_email_account.py b/frappe/email/doctype/email_account/test_email_account.py
index 35cacac45a..21dc4b84c4 100644
--- a/frappe/email/doctype/email_account/test_email_account.py
+++ b/frappe/email/doctype/email_account/test_email_account.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import os
import email
@@ -34,8 +34,8 @@ class TestEmailAccount(unittest.TestCase):
def setUp(self):
frappe.flags.mute_emails = False
frappe.flags.sent_mail = None
- frappe.db.sql('delete from `tabEmail Queue`')
- frappe.db.sql('delete from `tabUnhandled Email`')
+ frappe.db.delete("Email Queue")
+ frappe.db.delete("Unhandled Email")
def get_test_mail(self, fname):
with open(os.path.join(os.path.dirname(__file__), "test_mails", fname), "r") as f:
@@ -60,7 +60,7 @@ class TestEmailAccount(unittest.TestCase):
comm = frappe.get_doc("Communication", {"sender": "test_sender@example.com"})
comm.db_set("creation", datetime.now() - timedelta(seconds = 30 * 60))
- frappe.db.sql("DELETE FROM `tabEmail Queue`")
+ frappe.db.delete("Email Queue")
notify_unreplied()
self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": comm.reference_doctype,
"reference_name": comm.reference_name, "status":"Not Sent"}))
@@ -183,7 +183,7 @@ class TestEmailAccount(unittest.TestCase):
def test_threading_by_message_id(self):
cleanup()
- frappe.db.sql("""delete from `tabEmail Queue`""")
+ frappe.db.delete("Email Queue")
# reference document for testing
event = frappe.get_doc(dict(doctype='Event', subject='test-message')).insert()
@@ -242,8 +242,8 @@ class TestInboundMail(unittest.TestCase):
def setUp(self):
cleanup()
- frappe.db.sql('delete from `tabEmail Queue`')
- frappe.db.sql('delete from `tabToDo`')
+ frappe.db.delete("Email Queue")
+ frappe.db.delete("ToDo")
def get_test_mail(self, fname):
with open(os.path.join(os.path.dirname(__file__), "test_mails", fname), "r") as f:
diff --git a/frappe/email/doctype/email_domain/email_domain.py b/frappe/email/doctype/email_domain/email_domain.py
index 0856549eb7..1611d32351 100644
--- a/frappe/email/doctype/email_domain/email_domain.py
+++ b/frappe/email/doctype/email_domain/email_domain.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
diff --git a/frappe/email/doctype/email_domain/test_email_domain.py b/frappe/email/doctype/email_domain/test_email_domain.py
index 8607151ca8..1064c7684a 100644
--- a/frappe/email/doctype/email_domain/test_email_domain.py
+++ b/frappe/email/doctype/email_domain/test_email_domain.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
from frappe.test_runner import make_test_objects
diff --git a/frappe/email/doctype/email_flag_queue/email_flag_queue.py b/frappe/email/doctype/email_flag_queue/email_flag_queue.py
index 9bb30f08b2..886cf3c24b 100644
--- a/frappe/email/doctype/email_flag_queue/email_flag_queue.py
+++ b/frappe/email/doctype/email_flag_queue/email_flag_queue.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/email/doctype/email_flag_queue/test_email_flag_queue.py b/frappe/email/doctype/email_flag_queue/test_email_flag_queue.py
index d09b823ce6..b0e17b3b85 100644
--- a/frappe/email/doctype/email_flag_queue/test_email_flag_queue.py
+++ b/frappe/email/doctype/email_flag_queue/test_email_flag_queue.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/email/doctype/email_group/email_group.json b/frappe/email/doctype/email_group/email_group.json
index c49de841e6..cb74249143 100644
--- a/frappe/email/doctype/email_group/email_group.json
+++ b/frappe/email/doctype/email_group/email_group.json
@@ -1,6 +1,7 @@
{
"actions": [],
"allow_import": 1,
+ "allow_rename": 1,
"autoname": "field:title",
"creation": "2015-03-18 06:08:32.729800",
"doctype": "DocType",
@@ -50,7 +51,7 @@
"link_fieldname": "email_group"
}
],
- "modified": "2020-09-24 16:41:55.286377",
+ "modified": "2021-06-15 11:25:13.556201",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Group",
diff --git a/frappe/email/doctype/email_group/email_group.py b/frappe/email/doctype/email_group/email_group.py
index 2679353edf..ad52d9a9ec 100755
--- a/frappe/email/doctype/email_group/email_group.py
+++ b/frappe/email/doctype/email_group/email_group.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
diff --git a/frappe/email/doctype/email_group/test_email_group.py b/frappe/email/doctype/email_group/test_email_group.py
index 3e894118df..06341c128e 100644
--- a/frappe/email/doctype/email_group/test_email_group.py
+++ b/frappe/email/doctype/email_group/test_email_group.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/email/doctype/email_group_member/email_group_member.py b/frappe/email/doctype/email_group_member/email_group_member.py
index 1f9303b83e..a9fd26f710 100644
--- a/frappe/email/doctype/email_group_member/email_group_member.py
+++ b/frappe/email/doctype/email_group_member/email_group_member.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/email/doctype/email_group_member/test_email_group_member.py b/frappe/email/doctype/email_group_member/test_email_group_member.py
index 829d686400..de006dccb9 100644
--- a/frappe/email/doctype/email_group_member/test_email_group_member.py
+++ b/frappe/email/doctype/email_group_member/test_email_group_member.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py
index e1e332f978..4489a68cac 100644
--- a/frappe/email/doctype/email_queue/email_queue.py
+++ b/frappe/email/doctype/email_queue/email_queue.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import traceback
import json
diff --git a/frappe/email/doctype/email_queue/test_email_queue.py b/frappe/email/doctype/email_queue/test_email_queue.py
index b76d6347b9..8ebcb68a38 100644
--- a/frappe/email/doctype/email_queue/test_email_queue.py
+++ b/frappe/email/doctype/email_queue/test_email_queue.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/email/doctype/email_queue_recipient/email_queue_recipient.py b/frappe/email/doctype/email_queue_recipient/email_queue_recipient.py
index 055bdb3fc1..95b8593c4c 100644
--- a/frappe/email/doctype/email_queue_recipient/email_queue_recipient.py
+++ b/frappe/email/doctype/email_queue_recipient/email_queue_recipient.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/email/doctype/email_rule/email_rule.py b/frappe/email/doctype/email_rule/email_rule.py
index 9807724ef1..b2a4be5421 100644
--- a/frappe/email/doctype/email_rule/email_rule.py
+++ b/frappe/email/doctype/email_rule/email_rule.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/email/doctype/email_rule/test_email_rule.py b/frappe/email/doctype/email_rule/test_email_rule.py
index b2213f7405..eef5448e57 100644
--- a/frappe/email/doctype/email_rule/test_email_rule.py
+++ b/frappe/email/doctype/email_rule/test_email_rule.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/email/doctype/email_template/email_template.py b/frappe/email/doctype/email_template/email_template.py
index 4711451fd2..c51c46d72d 100644
--- a/frappe/email/doctype/email_template/email_template.py
+++ b/frappe/email/doctype/email_template/email_template.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe, json
from frappe.model.document import Document
diff --git a/frappe/email/doctype/email_template/test_email_template.py b/frappe/email/doctype/email_template/test_email_template.py
index 5a9ee969c6..a92ee9f9c3 100644
--- a/frappe/email/doctype/email_template/test_email_template.py
+++ b/frappe/email/doctype/email_template/test_email_template.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import unittest
class TestEmailTemplate(unittest.TestCase):
diff --git a/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py b/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py
index 6c47d8c538..d2ee828a55 100644
--- a/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py
+++ b/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/email/doctype/email_unsubscribe/test_email_unsubscribe.py b/frappe/email/doctype/email_unsubscribe/test_email_unsubscribe.py
index 602840fe3b..fdea802fdf 100644
--- a/frappe/email/doctype/email_unsubscribe/test_email_unsubscribe.py
+++ b/frappe/email/doctype/email_unsubscribe/test_email_unsubscribe.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/email/doctype/newsletter/exceptions.py b/frappe/email/doctype/newsletter/exceptions.py
new file mode 100644
index 0000000000..a6c688dbe8
--- /dev/null
+++ b/frappe/email/doctype/newsletter/exceptions.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See LICENSE
+
+from frappe.exceptions import ValidationError
+
+class NewsletterAlreadySentError(ValidationError):
+ pass
+
+class NoRecipientFoundError(ValidationError):
+ pass
+
+class NewsletterNotSavedError(ValidationError):
+ pass
diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py
index 97d77549b7..a118240488 100755
--- a/frappe/email/doctype/newsletter/newsletter.py
+++ b/frappe/email/doctype/newsletter/newsletter.py
@@ -1,241 +1,323 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See LICENSE
+
+from typing import Dict, List
import frappe
import frappe.utils
-from frappe import throw, _
+
+from frappe import _
from frappe.website.website_generator import WebsiteGenerator
from frappe.utils.verified_command import get_signed_params, verify_request
from frappe.email.doctype.email_group.email_group import add_subscribers
-from frappe.utils import parse_addr, now_datetime, markdown, validate_email_address
+
+from .exceptions import NewsletterAlreadySentError, NoRecipientFoundError, NewsletterNotSavedError
+
class Newsletter(WebsiteGenerator):
def onload(self):
- if self.email_sent:
- self.get("__onload").status_count = dict(frappe.db.sql("""select status, count(name)
- from `tabEmail Queue` where reference_doctype=%s and reference_name=%s
- group by status""", (self.doctype, self.name))) or None
+ self.setup_newsletter_status()
def validate(self):
- self.route = "newsletters/" + self.name
- if self.send_from:
- validate_email_address(self.send_from, True)
+ self.route = f"newsletters/{self.name}"
+ self.validate_sender_address()
+ self.validate_recipient_address()
+
+ @property
+ def newsletter_recipients(self) -> List[str]:
+ if getattr(self, "_recipients", None) is None:
+ self._recipients = self.get_recipients()
+ return self._recipients
@frappe.whitelist()
- def test_send(self, doctype="Lead"):
- self.recipients = frappe.utils.split_emails(self.test_email_id)
- self.queue_all(test_email=True)
+ def test_send(self):
+ test_emails = frappe.utils.split_emails(self.test_email_id)
+ self.queue_all(test_emails=test_emails)
frappe.msgprint(_("Test email sent to {0}").format(self.test_email_id))
@frappe.whitelist()
def send_emails(self):
"""send emails to leads and customers"""
+ self.queue_all()
+ frappe.msgprint(_("Email queued to {0} recipients").format(len(self.newsletter_recipients)))
+
+ def setup_newsletter_status(self):
+ """Setup analytical status for current Newsletter. Can be accessible from desk.
+ """
if self.email_sent:
- throw(_("Newsletter has already been sent"))
-
- self.recipients = self.get_recipients()
-
- if self.recipients:
- self.queue_all()
- frappe.msgprint(_("Email queued to {0} recipients").format(len(self.recipients)))
-
- else:
- frappe.msgprint(_("Newsletter should have atleast one recipient"))
-
- def queue_all(self, test_email=False):
- if not self.get("recipients"):
- # in case it is called via worker
- self.recipients = self.get_recipients()
-
- self.validate_send()
-
- sender = self.send_from or frappe.utils.get_formatted_email(self.owner)
-
- if not frappe.flags.in_test:
- frappe.db.auto_commit_on_many_writes = True
-
- attachments = []
- if self.send_attachments:
- files = frappe.get_all("File", fields=["name"], filters={"attached_to_doctype": "Newsletter",
- "attached_to_name": self.name}, order_by="creation desc")
-
- for file in files:
- try:
- # these attachments will be attached on-demand
- # and won't be stored in the message
- attachments.append({"fid": file.name})
- except IOError:
- frappe.throw(_("Unable to find attachment {0}").format(file.name))
-
- args = {
- "message": self.get_message(),
- "name": self.name
- }
- frappe.sendmail(recipients=self.recipients, sender=sender,
- subject=self.subject, message=self.get_message(), template="newsletter",
- reference_doctype=self.doctype, reference_name=self.name,
- add_unsubscribe_link=self.send_unsubscribe_link, attachments=attachments,
- unsubscribe_method="/unsubscribe",
- unsubscribe_params={"name": self.name},
- send_priority=0, queue_separately=True, args=args)
-
- if not frappe.flags.in_test:
- frappe.db.auto_commit_on_many_writes = False
-
- if not test_email:
- self.db_set("email_sent", 1)
- self.db_set("schedule_send", now_datetime())
- self.db_set("scheduled_to_send", len(self.recipients))
-
- def get_message(self):
- if self.content_type == "HTML":
- return frappe.render_template(self.message_html, {"doc": self.as_dict()})
- return {
- 'Rich Text': self.message,
- 'Markdown': markdown(self.message_md)
- }[self.content_type or 'Rich Text']
-
- def get_recipients(self):
- """Get recipients from Email Group"""
- recipients_list = []
- for email_group in get_email_groups(self.name):
- for d in frappe.db.get_all("Email Group Member", ["email"],
- {"unsubscribed": 0, "email_group": email_group.email_group}):
- recipients_list.append(d.email)
- return list(set(recipients_list))
+ status_count = frappe.get_all("Email Queue",
+ filters={"reference_doctype": self.doctype, "reference_name": self.name},
+ fields=["status", "count(name)"],
+ group_by="status",
+ order_by="status",
+ as_list=True,
+ )
+ self.get("__onload").status_count = dict(status_count)
def validate_send(self):
- if self.get("__islocal"):
- throw(_("Please save the Newsletter before sending"))
+ """Validate if Newsletter can be sent.
+ """
+ self.validate_newsletter_status()
+ self.validate_newsletter_recipients()
- if not self.recipients:
- frappe.throw(_("Newsletter should have at least one recipient"))
+ def validate_newsletter_status(self):
+ if self.email_sent:
+ frappe.throw(_("Newsletter has already been sent"), exc=NewsletterAlreadySentError)
+
+ if self.get("__islocal"):
+ frappe.throw(_("Please save the Newsletter before sending"), exc=NewsletterNotSavedError)
+
+ def validate_newsletter_recipients(self):
+ if not self.newsletter_recipients:
+ frappe.throw(_("Newsletter should have atleast one recipient"), exc=NoRecipientFoundError)
+ self.validate_recipient_address()
+
+ def validate_sender_address(self):
+ """Validate self.send_from is a valid email address or not.
+ """
+ if self.send_from:
+ frappe.utils.validate_email_address(self.send_from, throw=True)
+
+ def validate_recipient_address(self):
+ """Validate if self.newsletter_recipients are all valid email addresses or not.
+ """
+ for recipient in self.newsletter_recipients:
+ frappe.utils.validate_email_address(recipient, throw=True)
+
+ def get_linked_email_queue(self) -> List[str]:
+ """Get list of email queue linked to this newsletter.
+ """
+ return frappe.get_all("Email Queue",
+ filters={
+ "reference_doctype": self.doctype,
+ "reference_name": self.name,
+ },
+ pluck="name",
+ )
+
+ def get_success_recipients(self) -> List[str]:
+ """Recipients who have already recieved the newsletter.
+
+ Couldn't think of a better name ;)
+ """
+ return frappe.get_all("Email Queue Recipient",
+ filters={
+ "status": ("in", ["Not Sent", "Sending", "Sent"]),
+ "parentfield": ("in", self.get_linked_email_queue()),
+ },
+ pluck="recipient",
+ )
+
+ def get_pending_recipients(self) -> List[str]:
+ """Get list of pending recipients of the newsletter. These
+ recipients may not have receive the newsletter in the previous iteration.
+ """
+ return [
+ x for x in self.newsletter_recipients if x not in self.get_success_recipients()
+ ]
+
+ def queue_all(self, test_emails: List[str] = None):
+ """Queue Newsletter to all the recipients generated from the `Email Group`
+ table
+
+ Args:
+ test_email (List[str], optional): Send test Newsletter to the passed set of emails.
+ Defaults to None.
+ """
+ if test_emails:
+ for test_email in test_emails:
+ frappe.utils.validate_email_address(test_email, throw=True)
+ else:
+ self.validate()
+ self.validate_send()
+
+ newsletter_recipients = test_emails or self.get_pending_recipients()
+ self.send_newsletter(emails=newsletter_recipients)
+
+ if not test_emails:
+ self.email_sent = True
+ self.schedule_send = frappe.utils.now_datetime()
+ self.scheduled_to_send = len(newsletter_recipients)
+ self.save()
+
+ def get_newsletter_attachments(self) -> List[Dict[str, str]]:
+ """Get list of attachments on current Newsletter
+ """
+ attachments = []
+
+ if self.send_attachments:
+ files = frappe.get_all(
+ "File",
+ filters={"attached_to_doctype": "Newsletter", "attached_to_name": self.name},
+ order_by="creation desc",
+ pluck="name",
+ )
+ attachments.extend({"fid": file} for file in files)
+
+ return attachments
+
+ def send_newsletter(self, emails: List[str]):
+ """Trigger email generation for `emails` and add it in Email Queue.
+ """
+ # TODO: get rid of this maybe?
+ message = self.get_message()
+ attachments = self.get_newsletter_attachments()
+ sender = self.send_from or frappe.utils.get_formatted_email(self.owner)
+ args = {"message": message, "name": self.name}
+
+ is_auto_commit_set = bool(frappe.db.auto_commit_on_many_writes)
+ frappe.db.auto_commit_on_many_writes = not frappe.flags.in_test
+
+ frappe.sendmail(
+ subject=self.subject,
+ sender=sender,
+ recipients=emails,
+ message=message,
+ attachments=attachments,
+ template="newsletter",
+ add_unsubscribe_link=self.send_unsubscribe_link,
+ unsubscribe_method="/unsubscribe",
+ unsubscribe_params={"name": self.name},
+ reference_doctype=self.doctype,
+ reference_name=self.name,
+ queue_separately=True,
+ send_priority=0,
+ args=args,
+ )
+
+ frappe.db.auto_commit_on_many_writes = is_auto_commit_set
+
+ def get_message(self) -> str:
+ if self.content_type == "HTML":
+ return frappe.render_template(self.message_html, {"doc": self.as_dict()})
+ if self.content_type == "Markdown":
+ return frappe.utils.markdown(self.message_md)
+ # fallback to Rich Text
+ return self.message
+
+ def get_recipients(self) -> List[str]:
+ """Get recipients from Email Group"""
+ emails = frappe.get_all(
+ "Email Group Member",
+ filters={"unsubscribed": 0, "email_group": ("in", self.get_email_groups())},
+ pluck="email",
+ )
+ return list(set(emails))
+
+ def get_email_groups(self) -> List[str]:
+ # wondering why the 'or'? i can't figure out why both aren't equivalent - @gavin
+ return [
+ x.email_group for x in self.email_group
+ ] or frappe.get_all(
+ "Newsletter Email Group",
+ filters={"parent": self.name, "parenttype": "Newsletter"},
+ pluck="email_group",
+ )
+
+ def get_attachments(self) -> List[Dict[str, str]]:
+ return frappe.get_all(
+ "File",
+ fields=["name", "file_name", "file_url", "is_private"],
+ filters={
+ "attached_to_name": self.name,
+ "attached_to_doctype": "Newsletter",
+ "is_private": 0,
+ },
+ )
def get_context(self, context):
newsletters = get_newsletter_list("Newsletter", None, None, 0)
if newsletters:
newsletter_list = [d.name for d in newsletters]
if self.name not in newsletter_list:
- frappe.redirect_to_message(_('Permission Error'),
- _("You are not permitted to view the newsletter."))
+ frappe.redirect_to_message(
+ _("Permission Error"), _("You are not permitted to view the newsletter.")
+ )
frappe.local.flags.redirect_location = frappe.local.response.location
raise frappe.Redirect
else:
- context.attachments = get_attachments(self.name)
+ context.attachments = self.get_attachments()
context.no_cache = 1
context.show_sidebar = True
-def get_attachments(name):
- return frappe.get_all("File",
- fields=["name", "file_name", "file_url", "is_private"],
- filters = {"attached_to_name": name, "attached_to_doctype": "Newsletter", "is_private":0})
-
-
-def get_email_groups(name):
- return frappe.db.get_all("Newsletter Email Group", ["email_group"],{"parent":name, "parenttype":"Newsletter"})
-
-
@frappe.whitelist(allow_guest=True)
def confirmed_unsubscribe(email, group):
""" unsubscribe the email(user) from the mailing list(email_group) """
- frappe.flags.ignore_permissions=True
+ frappe.flags.ignore_permissions = True
doc = frappe.get_doc("Email Group Member", {"email": email, "email_group": group})
if not doc.unsubscribed:
doc.unsubscribed = 1
- doc.save(ignore_permissions = True)
-
-def create_lead(email_id):
- """create a lead if it does not exist"""
- from frappe.model.naming import get_default_naming_series
- full_name, email_id = parse_addr(email_id)
- if frappe.db.get_value("Lead", {"email_id": email_id}):
- return
-
- lead = frappe.get_doc({
- "doctype": "Lead",
- "email_id": email_id,
- "lead_name": full_name or email_id,
- "status": "Lead",
- "naming_series": get_default_naming_series("Lead"),
- "company": frappe.db.get_default("Company"),
- "source": "Email"
- })
- lead.insert()
+ doc.save(ignore_permissions=True)
@frappe.whitelist(allow_guest=True)
-def subscribe(email, email_group=_('Website')):
- url = frappe.utils.get_url("/api/method/frappe.email.doctype.newsletter.newsletter.confirm_subscription") +\
- "?" + get_signed_params({"email": email, "email_group": email_group})
+def subscribe(email, email_group=_("Website")):
+ """API endpoint to subscribe an email to a particular email group. Triggers a confirmation email.
+ """
- email_template = frappe.db.get_value('Email Group', email_group, ['confirmation_email_template'])
+ # build subscription confirmation URL
+ api_endpoint = frappe.utils.get_url(
+ "/api/method/frappe.email.doctype.newsletter.newsletter.confirm_subscription"
+ )
+ signed_params = get_signed_params({"email": email, "email_group": email_group})
+ confirm_subscription_url = f"{api_endpoint}?{signed_params}"
- content=''
- if email_template:
- args = dict(
- email=email,
- confirmation_url=url,
- email_group=email_group
- )
+ # fetch custom template if available
+ email_confirmation_template = frappe.db.get_value(
+ "Email Group", email_group, "confirmation_email_template"
+ )
- email_template = frappe.get_doc("Email Template", email_template)
+ # build email and send
+ if email_confirmation_template:
+ args = {"email": email, "confirmation_url": confirm_subscription_url, "email_group": email_group}
+ email_template = frappe.get_doc("Email Template", email_confirmation_template)
+ email_subject = email_template.subject
content = frappe.render_template(email_template.response, args)
-
- if not content:
- messages = (
+ else:
+ email_subject = _("Confirm Your Email")
+ translatable_content = (
_("Thank you for your interest in subscribing to our updates"),
_("Please verify your Email Address"),
- url,
- _("Click here to verify")
+ confirm_subscription_url,
+ _("Click here to verify"),
)
-
content = """
- {0}. {1}.
- {3}
- """.format(*messages)
+ {0}. {1}.
+ {3}
+ """.format(*translatable_content)
+
+ frappe.sendmail(
+ email,
+ subject=email_subject,
+ content=content,
+ now=True,
+ )
- frappe.sendmail(email, subject=getattr('email_template', 'subject', '') or _("Confirm Your Email"), content=content, now=True)
@frappe.whitelist(allow_guest=True)
-def confirm_subscription(email, email_group=_('Website')):
+def confirm_subscription(email, email_group=_("Website")):
+ """API endpoint to confirm email subscription.
+ This endpoint is called when user clicks on the link sent to their mail.
+ """
if not verify_request():
return
if not frappe.db.exists("Email Group", email_group):
- frappe.get_doc({
- "doctype": "Email Group",
- "title": email_group
- }).insert(ignore_permissions=True)
+ frappe.get_doc({"doctype": "Email Group", "title": email_group}).insert(
+ ignore_permissions=True
+ )
frappe.flags.ignore_permissions = True
add_subscribers(email_group, email)
frappe.db.commit()
- frappe.respond_as_web_page(_("Confirmed"),
+ frappe.respond_as_web_page(
+ _("Confirmed"),
_("{0} has been successfully added to the Email Group.").format(email),
- indicator_color='green')
-
-
-def send_newsletter(newsletter):
- try:
- doc = frappe.get_doc("Newsletter", newsletter)
- doc.queue_all()
-
- except:
- frappe.db.rollback()
-
- # wasn't able to send emails :(
- doc.db_set("email_sent", 0)
- frappe.db.commit()
-
- frappe.log_error(title='Send Newsletter')
-
- raise
-
- else:
- frappe.db.commit()
+ indicator_color="green",
+ )
def get_list_context(context=None):
@@ -268,12 +350,35 @@ def get_newsletter_list(doctype, txt, filters, limit_start, limit_page_length=20
'''.format(','.join(['%s'] * len(email_group_list)),
limit_page_length, limit_start), email_group_list, as_dict=1)
+
def send_scheduled_email():
"""Send scheduled newsletter to the recipients."""
- scheduled_newsletter = frappe.get_all('Newsletter', filters = {
- 'schedule_send': ('<=', now_datetime()),
- 'email_sent': 0,
- 'schedule_sending': 1
- }, fields = ['name'], ignore_ifnull=True)
+ scheduled_newsletter = frappe.get_all(
+ "Newsletter",
+ filters={
+ "schedule_send": ("<=", frappe.utils.now_datetime()),
+ "email_sent": False,
+ "schedule_sending": True,
+ },
+ ignore_ifnull=True,
+ pluck="name",
+ )
+
for newsletter in scheduled_newsletter:
- send_newsletter(newsletter.name)
+ try:
+ frappe.get_doc("Newsletter", newsletter).queue_all()
+
+ except Exception:
+ frappe.db.rollback()
+
+ # wasn't able to send emails :(
+ frappe.db.set_value("Newsletter", newsletter, "email_sent", 0)
+ message = (
+ f"Newsletter {newsletter} failed to send"
+ "\n\n"
+ f"Traceback: {frappe.get_traceback()}"
+ )
+ frappe.log_error(title="Send Newsletter", message=message)
+
+ if not frappe.flags.in_test:
+ frappe.db.commit()
diff --git a/frappe/email/doctype/newsletter/test_newsletter.py b/frappe/email/doctype/newsletter/test_newsletter.py
index 3abd339ed9..abbcc6440c 100644
--- a/frappe/email/doctype/newsletter/test_newsletter.py
+++ b/frappe/email/doctype/newsletter/test_newsletter.py
@@ -1,17 +1,26 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See LICENSE
+
import unittest
from random import choice
+from typing import Union
+from unittest.mock import MagicMock, PropertyMock, patch
import frappe
-from frappe.email.doctype.newsletter.newsletter import (
- confirmed_unsubscribe,
- send_scheduled_email,
+from frappe.desk.form.load import run_onload
+from frappe.email.doctype.newsletter.exceptions import (
+ NewsletterAlreadySentError, NoRecipientFoundError
+)
+from frappe.email.doctype.newsletter.newsletter import (
+ Newsletter,
+ confirmed_unsubscribe,
+ get_newsletter_list,
+ send_scheduled_email
)
-from frappe.email.doctype.newsletter.newsletter import get_newsletter_list
from frappe.email.queue import flush
from frappe.utils import add_days, getdate
+
test_dependencies = ["Email Group"]
emails = [
"test_subscriber1@example.com",
@@ -19,23 +28,107 @@ emails = [
"test_subscriber3@example.com",
"test1@example.com",
]
+newsletters = []
-class TestNewsletter(unittest.TestCase):
+def get_dotted_path(obj: type) -> str:
+ klass = obj.__class__
+ module = klass.__module__
+ if module == 'builtins':
+ return klass.__qualname__ # avoid outputs like 'builtins.str'
+ return f"{module}.{klass.__qualname__}"
+
+
+class TestNewsletterMixin:
def setUp(self):
frappe.set_user("Administrator")
- frappe.db.sql("delete from `tabEmail Group Member`")
+ self.setup_email_group()
+ def tearDown(self):
+ frappe.set_user("Administrator")
+ for newsletter in newsletters:
+ frappe.db.delete("Email Queue", {
+ "reference_doctype": "Newsletter",
+ "reference_name": newsletter,
+ })
+ frappe.delete_doc("Newsletter", newsletter)
+ frappe.db.delete("Newsletter Email Group", newsletter)
+ newsletters.remove(newsletter)
+
+ def setup_email_group(self):
if not frappe.db.exists("Email Group", "_Test Email Group"):
- frappe.get_doc({"doctype": "Email Group", "title": "_Test Email Group"}).insert()
-
- for email in emails:
frappe.get_doc({
- "doctype": "Email Group Member",
- "email": email,
- "email_group": "_Test Email Group"
+ "doctype": "Email Group",
+ "title": "_Test Email Group"
}).insert()
+ for email in emails:
+ doctype = "Email Group Member"
+ email_filters = {
+ "email": email,
+ "email_group": "_Test Email Group"
+ }
+ try:
+ frappe.get_doc({
+ "doctype": doctype,
+ **email_filters,
+ }).insert()
+ except Exception:
+ frappe.db.update(doctype, email_filters, "unsubscribed", 0)
+
+ def send_newsletter(self, published=0, schedule_send=None) -> Union[str, None]:
+ frappe.db.delete("Email Queue")
+ frappe.db.delete("Email Queue Recipient")
+ frappe.db.delete("Newsletter")
+
+ newsletter_options = {
+ "published": published,
+ "schedule_sending": bool(schedule_send),
+ "schedule_send": schedule_send
+ }
+ newsletter = self.get_newsletter(**newsletter_options)
+
+ if schedule_send:
+ send_scheduled_email()
+ else:
+ newsletter.send_emails()
+ return newsletter.name
+
+ @staticmethod
+ def get_newsletter(**kwargs) -> "Newsletter":
+ """Generate and return Newsletter object
+ """
+ doctype = "Newsletter"
+ newsletter_content = {
+ "subject": "_Test Newsletter",
+ "send_from": "Test Sender ",
+ "content_type": "Rich Text",
+ "message": "Testing my news.",
+ }
+ similar_newsletters = frappe.db.get_all(doctype, newsletter_content, pluck="name")
+
+ for similar_newsletter in similar_newsletters:
+ frappe.delete_doc(doctype, similar_newsletter)
+
+ newsletter = frappe.get_doc({"doctype": doctype, **newsletter_content, **kwargs})
+ newsletter.append("email_group", {"email_group": "_Test Email Group"})
+ newsletter.save(ignore_permissions=True)
+ newsletter.reload()
+ newsletters.append(newsletter.name)
+
+ attached_files = frappe.get_all("File", {
+ "attached_to_doctype": newsletter.doctype,
+ "attached_to_name": newsletter.name,
+ },
+ pluck="name",
+ )
+ for file in attached_files:
+ frappe.delete_doc("File", file)
+
+ return newsletter
+
+
+class TestNewsletter(TestNewsletterMixin, unittest.TestCase):
def test_send(self):
self.send_newsletter()
@@ -64,40 +157,15 @@ class TestNewsletter(unittest.TestCase):
if email != to_unsubscribe:
self.assertTrue(email in recipients)
- @staticmethod
- def send_newsletter(published=0, schedule_send=None):
- frappe.db.sql("delete from `tabEmail Queue`")
- frappe.db.sql("delete from `tabEmail Queue Recipient`")
- frappe.db.sql("delete from `tabNewsletter`")
- newsletter = frappe.get_doc({
- "doctype": "Newsletter",
- "subject": "_Test Newsletter",
- "send_from": "Test Sender ",
- "content_type": "Rich Text",
- "message": "Testing my news.",
- "published": published,
- "schedule_sending": bool(schedule_send),
- "schedule_send": schedule_send
- }).insert(ignore_permissions=True)
-
- newsletter.append("email_group", {"email_group": "_Test Email Group"})
- newsletter.save()
- if schedule_send:
- send_scheduled_email()
- return
-
- newsletter.send_emails()
- return newsletter.name
-
def test_portal(self):
- self.send_newsletter(1)
+ self.send_newsletter(published=1)
frappe.set_user("test1@example.com")
- newsletters = get_newsletter_list("Newsletter", None, None, 0)
- self.assertEqual(len(newsletters), 1)
+ newsletter_list = get_newsletter_list("Newsletter", None, None, 0)
+ self.assertEqual(len(newsletter_list), 1)
def test_newsletter_context(self):
context = frappe._dict()
- newsletter_name = self.send_newsletter(1)
+ newsletter_name = self.send_newsletter(published=1)
frappe.set_user("test2@example.com")
doc = frappe.get_doc("Newsletter", newsletter_name)
doc.get_context(context)
@@ -112,3 +180,68 @@ class TestNewsletter(unittest.TestCase):
recipients = [e.recipients[0].recipient for e in email_queue_list]
for email in emails:
self.assertTrue(email in recipients)
+
+ def test_newsletter_test_send(self):
+ """Test "Test Send" functionality of Newsletter
+ """
+ newsletter = self.get_newsletter()
+ newsletter.test_email_id = choice(emails)
+ newsletter.test_send()
+
+ self.assertFalse(newsletter.email_sent)
+ newsletter.save = MagicMock()
+ self.assertFalse(newsletter.save.called)
+
+ def test_newsletter_status(self):
+ """Test for Newsletter's stats on onload event
+ """
+ newsletter = self.get_newsletter()
+ newsletter.email_sent = True
+ # had to use run_onload as calling .onload directly bought weird errors
+ # like TestNewsletter has no attribute "_TestNewsletter__onload"
+ run_onload(newsletter)
+ self.assertIsInstance(newsletter.get("__onload").status_count, dict)
+
+ def test_already_sent_newsletter(self):
+ newsletter = self.get_newsletter()
+ newsletter.send_emails()
+
+ with self.assertRaises(NewsletterAlreadySentError):
+ newsletter.send_emails()
+
+ def test_newsletter_with_no_recipient(self):
+ newsletter = self.get_newsletter()
+ property_path = f"{get_dotted_path(newsletter)}.newsletter_recipients"
+
+ with patch(property_path, new_callable=PropertyMock) as mock_newsletter_recipients:
+ mock_newsletter_recipients.return_value = []
+ with self.assertRaises(NoRecipientFoundError):
+ newsletter.send_emails()
+
+ def test_send_newsletter_with_attachments(self):
+ newsletter = self.get_newsletter()
+ newsletter.reload()
+ file_attachment = frappe.get_doc({
+ "doctype": "File",
+ "file_name": "test1.txt",
+ "attached_to_doctype": newsletter.doctype,
+ "attached_to_name": newsletter.name,
+ "content": frappe.mock("paragraph")
+ })
+ file_attachment.save()
+ newsletter.send_attachments = True
+ newsletter_attachments = newsletter.get_newsletter_attachments()
+ self.assertEqual(len(newsletter_attachments), 1)
+ self.assertEqual(newsletter_attachments[0]["fid"], file_attachment.name)
+
+ def test_send_scheduled_email_error_handling(self):
+ newsletter = self.get_newsletter(schedule_send=add_days(getdate(), -1))
+ job_path = "frappe.email.doctype.newsletter.newsletter.Newsletter.queue_all"
+ m = MagicMock(side_effect=frappe.OutgoingEmailError)
+
+ with self.assertRaises(frappe.OutgoingEmailError):
+ with patch(job_path, new_callable=m):
+ send_scheduled_email()
+
+ newsletter.reload()
+ self.assertEqual(newsletter.email_sent, 0)
diff --git a/frappe/email/doctype/newsletter_email_group/newsletter_email_group.py b/frappe/email/doctype/newsletter_email_group/newsletter_email_group.py
index a453dda9e4..89476c4d53 100644
--- a/frappe/email/doctype/newsletter_email_group/newsletter_email_group.py
+++ b/frappe/email/doctype/newsletter_email_group/newsletter_email_group.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py
index 57418515f5..6b4ee92043 100644
--- a/frappe/email/doctype/notification/notification.py
+++ b/frappe/email/doctype/notification/notification.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
import json, os
@@ -146,6 +146,7 @@ def get_context(context):
if doc.meta.get_field(fieldname).fieldtype in frappe.model.numeric_fieldtypes:
value = frappe.utils.cint(value)
+ doc.reload()
doc.set(fieldname, value)
doc.flags.updater_reference = {
'doctype': self.doctype,
diff --git a/frappe/email/doctype/notification/test_notification.py b/frappe/email/doctype/notification/test_notification.py
index d6358ccbbe..f05d35be3e 100644
--- a/frappe/email/doctype/notification/test_notification.py
+++ b/frappe/email/doctype/notification/test_notification.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe, frappe.utils, frappe.utils.scheduler
from frappe.desk.form import assign_to
import unittest
@@ -9,7 +9,7 @@ test_dependencies = ["User", "Notification"]
class TestNotification(unittest.TestCase):
def setUp(self):
- frappe.db.sql("""delete from `tabEmail Queue`""")
+ frappe.db.delete("Email Queue")
frappe.set_user("test@example.com")
if not frappe.db.exists('Notification', {'name': 'ToDo Status Update'}, 'name'):
@@ -20,6 +20,8 @@ class TestNotification(unittest.TestCase):
notification.event = 'Value Change'
notification.value_changed = 'status'
notification.send_to_all_assignees = 1
+ notification.set_property_after_alert = 'description'
+ notification.property_value = 'Changed by Notification'
notification.save()
if not frappe.db.exists('Notification', {'name': 'Contact Status Update'}, 'name'):
@@ -50,7 +52,7 @@ class TestNotification(unittest.TestCase):
self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": "Communication",
"reference_name": communication.name, "status":"Not Sent"}))
- frappe.db.sql("""delete from `tabEmail Queue`""")
+ frappe.db.delete("Email Queue")
communication.reload()
communication.content = "test 2"
@@ -189,9 +191,9 @@ class TestNotification(unittest.TestCase):
def test_cc_jinja(self):
- frappe.db.sql("""delete from `tabUser` where email='test_jinja@example.com'""")
- frappe.db.sql("""delete from `tabEmail Queue`""")
- frappe.db.sql("""delete from `tabEmail Queue Recipient`""")
+ frappe.db.delete("User", {"email": "test_jinja@example.com"})
+ frappe.db.delete("Email Queue")
+ frappe.db.delete("Email Queue Recipient")
test_user = frappe.new_doc("User")
test_user.name = 'test_jinja'
@@ -205,9 +207,9 @@ class TestNotification(unittest.TestCase):
self.assertTrue(frappe.db.get_value("Email Queue Recipient", {"recipient": "test_jinja@example.com"}))
- frappe.db.sql("""delete from `tabUser` where email='test_jinja@example.com'""")
- frappe.db.sql("""delete from `tabEmail Queue`""")
- frappe.db.sql("""delete from `tabEmail Queue Recipient`""")
+ frappe.db.delete("User", {"email": "test_jinja@example.com"})
+ frappe.db.delete("Email Queue")
+ frappe.db.delete("Email Queue Recipient")
def test_notification_to_assignee(self):
todo = frappe.new_doc('ToDo')
@@ -237,6 +239,9 @@ class TestNotification(unittest.TestCase):
self.assertTrue(email_queue)
+ # check if description is changed after alert since set_property_after_alert is set
+ self.assertEquals(todo.description, 'Changed by Notification')
+
recipients = [d.recipient for d in email_queue.recipients]
self.assertTrue('test2@example.com' in recipients)
self.assertTrue('test1@example.com' in recipients)
@@ -269,4 +274,7 @@ class TestNotification(unittest.TestCase):
self.assertTrue('test2@example.com' in recipients)
self.assertTrue('test1@example.com' in recipients)
-
+ @classmethod
+ def tearDownClass(cls):
+ frappe.delete_doc_if_exists("Notification", "ToDo Status Update")
+ frappe.delete_doc_if_exists("Notification", "Contact Status Update")
\ No newline at end of file
diff --git a/frappe/email/doctype/notification_recipient/notification_recipient.py b/frappe/email/doctype/notification_recipient/notification_recipient.py
index d8480c5455..68871e5047 100644
--- a/frappe/email/doctype/notification_recipient/notification_recipient.py
+++ b/frappe/email/doctype/notification_recipient/notification_recipient.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/email/doctype/unhandled_email/test_unhandled_email.py b/frappe/email/doctype/unhandled_email/test_unhandled_email.py
index 5606b8ff30..37c65584e0 100644
--- a/frappe/email/doctype/unhandled_email/test_unhandled_email.py
+++ b/frappe/email/doctype/unhandled_email/test_unhandled_email.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/email/doctype/unhandled_email/unhandled_email.py b/frappe/email/doctype/unhandled_email/unhandled_email.py
index 6414dbece3..db14a50d09 100644
--- a/frappe/email/doctype/unhandled_email/unhandled_email.py
+++ b/frappe/email/doctype/unhandled_email/unhandled_email.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
@@ -10,5 +10,6 @@ class UnhandledEmail(Document):
def remove_old_unhandled_emails():
- frappe.db.sql("""DELETE FROM `tabUnhandled Email`
- WHERE creation < %s""", frappe.utils.add_days(frappe.utils.nowdate(), -30))
+ frappe.db.delete("Unhandled Email", {
+ "creation": ("<", frappe.utils.add_days(frappe.utils.nowdate(), -30))
+ })
\ No newline at end of file
diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py
index ffb44d3412..c25e996bd3 100755
--- a/frappe/email/email_body.py
+++ b/frappe/email/email_body.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe, re, os
from frappe.utils.pdf import get_pdf
@@ -13,8 +13,8 @@ from email import policy
def get_email(recipients, sender='', msg='', subject='[No Subject]',
text_content = None, footer=None, print_html=None, formatted=None, attachments=None,
- content=None, reply_to=None, cc=[], bcc=[], email_account=None, expose_recipients=None,
- inline_images=[], header=None):
+ content=None, reply_to=None, cc=None, bcc=None, email_account=None, expose_recipients=None,
+ inline_images=None, header=None):
""" Prepare an email with the following format:
- multipart/mixed
- multipart/alternative
@@ -25,6 +25,14 @@ def get_email(recipients, sender='', msg='', subject='[No Subject]',
- attachment
"""
content = content or msg
+
+ if cc is None:
+ cc = []
+ if bcc is None:
+ bcc = []
+ if inline_images is None:
+ inline_images = []
+
emailobj = EMail(sender, recipients, subject, reply_to=reply_to, cc=cc, bcc=bcc, email_account=email_account, expose_recipients=expose_recipients)
if not content.strip().startswith("<"):
diff --git a/frappe/email/queue.py b/frappe/email/queue.py
index 885a306cfb..16e3fecf48 100755
--- a/frappe/email/queue.py
+++ b/frappe/email/queue.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import msgprint, _
@@ -173,13 +173,8 @@ def clear_outbox(days=None):
WHERE `priority`=0 AND `modified` < (NOW() - INTERVAL '{0}' DAY)""".format(days))
if email_queues:
- frappe.db.sql("""DELETE FROM `tabEmail Queue` WHERE `name` IN ({0})""".format(
- ','.join(['%s']*len(email_queues)
- )), tuple(email_queues))
-
- frappe.db.sql("""DELETE FROM `tabEmail Queue Recipient` WHERE `parent` IN ({0})""".format(
- ','.join(['%s']*len(email_queues)
- )), tuple(email_queues))
+ frappe.db.delete("Email Queue", {"name": ("in", email_queues)})
+ frappe.db.delete("Email Queue Recipient", {"parent": ("in", email_queues)})
def set_expiry_for_email_queue():
''' Mark emails as expire that has not sent for 7 days.
diff --git a/frappe/email/receive.py b/frappe/email/receive.py
index 9ad560aa4a..7fab90bee3 100644
--- a/frappe/email/receive.py
+++ b/frappe/email/receive.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import datetime
import email
@@ -340,7 +340,7 @@ class EmailServer:
return error_msg
- def update_flag(self, uid_list={}):
+ def update_flag(self, uid_list=None):
""" set all uids mails the flag as seen """
if not uid_list:
@@ -802,7 +802,7 @@ class InboundMail(Email):
except frappe.DuplicateEntryError:
# try and find matching parent
parent_name = frappe.db.get_value(self.email_account.append_to,
- {email_fileds.sender_field: email.from_email}
+ {email_fileds.sender_field: self.from_email}
)
if parent_name:
parent.name = parent_name
diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py
index 74492c09c3..6f73a73f11 100644
--- a/frappe/email/smtp.py
+++ b/frappe/email/smtp.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import smtplib
diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py
index 8e637273ed..c542bc2578 100644
--- a/frappe/email/test_email_body.py
+++ b/frappe/email/test_email_body.py
@@ -1,5 +1,6 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# License: MIT. See LICENSE
+
import unittest, os, base64
from frappe import safe_decode
from frappe.email.receive import Email
@@ -127,7 +128,7 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
'''
transformed_html = '''
Hi John
-This is a test email
+This is a test email
'''
self.assertTrue(transformed_html in inline_style_in_html(html))
diff --git a/frappe/email/utils.py b/frappe/email/utils.py
index 24ce77b922..1138698491 100644
--- a/frappe/email/utils.py
+++ b/frappe/email/utils.py
@@ -1,5 +1,5 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import imaplib, poplib
from frappe.utils import cint
diff --git a/frappe/event_streaming/doctype/document_type_field_mapping/document_type_field_mapping.py b/frappe/event_streaming/doctype/document_type_field_mapping/document_type_field_mapping.py
index fc8164d8a4..3019d70035 100644
--- a/frappe/event_streaming/doctype/document_type_field_mapping/document_type_field_mapping.py
+++ b/frappe/event_streaming/doctype/document_type_field_mapping/document_type_field_mapping.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/event_streaming/doctype/document_type_mapping/document_type_mapping.py b/frappe/event_streaming/doctype/document_type_mapping/document_type_mapping.py
index 2cf7282a5a..8f1e5504da 100644
--- a/frappe/event_streaming/doctype/document_type_mapping/document_type_mapping.py
+++ b/frappe/event_streaming/doctype/document_type_mapping/document_type_mapping.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
import json
from frappe import _
diff --git a/frappe/event_streaming/doctype/document_type_mapping/test_document_type_mapping.py b/frappe/event_streaming/doctype/document_type_mapping/test_document_type_mapping.py
index b1bb322855..a277139985 100644
--- a/frappe/event_streaming/doctype/document_type_mapping/test_document_type_mapping.py
+++ b/frappe/event_streaming/doctype/document_type_mapping/test_document_type_mapping.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/event_streaming/doctype/event_consumer/event_consumer.py b/frappe/event_streaming/doctype/event_consumer/event_consumer.py
index 00d304f7f4..e8b84d1345 100644
--- a/frappe/event_streaming/doctype/event_consumer/event_consumer.py
+++ b/frappe/event_streaming/doctype/event_consumer/event_consumer.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
import json
diff --git a/frappe/event_streaming/doctype/event_consumer/test_event_consumer.py b/frappe/event_streaming/doctype/event_consumer/test_event_consumer.py
index b8072ecabd..11c69e7ba3 100644
--- a/frappe/event_streaming/doctype/event_consumer/test_event_consumer.py
+++ b/frappe/event_streaming/doctype/event_consumer/test_event_consumer.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.py b/frappe/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.py
index cf5d18edfd..b33313087f 100644
--- a/frappe/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.py
+++ b/frappe/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.py b/frappe/event_streaming/doctype/event_producer/event_producer.py
index 4836276734..05771a89d3 100644
--- a/frappe/event_streaming/doctype/event_producer/event_producer.py
+++ b/frappe/event_streaming/doctype/event_producer/event_producer.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import json
import time
@@ -408,8 +408,9 @@ def sync_dependencies(document, producer_site):
child_table = doc.get(df.fieldname)
for entry in child_table:
child_doc = producer_site.get_doc(entry.doctype, entry.name)
- child_doc = frappe._dict(child_doc)
- set_dependencies(child_doc, frappe.get_meta(entry.doctype).get_link_fields(), producer_site)
+ if child_doc:
+ child_doc = frappe._dict(child_doc)
+ set_dependencies(child_doc, frappe.get_meta(entry.doctype).get_link_fields(), producer_site)
def sync_link_dependencies(doc, link_fields, producer_site):
set_dependencies(doc, link_fields, producer_site)
diff --git a/frappe/event_streaming/doctype/event_producer/test_event_producer.py b/frappe/event_streaming/doctype/event_producer/test_event_producer.py
index 883f4f2df2..3d697ceb3a 100644
--- a/frappe/event_streaming/doctype/event_producer/test_event_producer.py
+++ b/frappe/event_streaming/doctype/event_producer/test_event_producer.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
import json
diff --git a/frappe/event_streaming/doctype/event_producer_document_type/event_producer_document_type.py b/frappe/event_streaming/doctype/event_producer_document_type/event_producer_document_type.py
index 9ae70e0f97..3e9623f56f 100644
--- a/frappe/event_streaming/doctype/event_producer_document_type/event_producer_document_type.py
+++ b/frappe/event_streaming/doctype/event_producer_document_type/event_producer_document_type.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py
index 391cf79c27..0868e86253 100644
--- a/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py
+++ b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py b/frappe/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py
index 62ea71edab..c2d943a463 100644
--- a/frappe/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py
+++ b/frappe/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/event_streaming/doctype/event_sync_log/event_sync_log.py b/frappe/event_streaming/doctype/event_sync_log/event_sync_log.py
index 1d255a5c30..c26ca46e05 100644
--- a/frappe/event_streaming/doctype/event_sync_log/event_sync_log.py
+++ b/frappe/event_streaming/doctype/event_sync_log/event_sync_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/event_streaming/doctype/event_sync_log/test_event_sync_log.py b/frappe/event_streaming/doctype/event_sync_log/test_event_sync_log.py
index ef55dc0f16..b901f92ef8 100644
--- a/frappe/event_streaming/doctype/event_sync_log/test_event_sync_log.py
+++ b/frappe/event_streaming/doctype/event_sync_log/test_event_sync_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/event_streaming/doctype/event_update_log/event_update_log.py b/frappe/event_streaming/doctype/event_update_log/event_update_log.py
index ae851c70d1..f4871be312 100644
--- a/frappe/event_streaming/doctype/event_update_log/event_update_log.py
+++ b/frappe/event_streaming/doctype/event_update_log/event_update_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/event_streaming/doctype/event_update_log/test_event_update_log.py b/frappe/event_streaming/doctype/event_update_log/test_event_update_log.py
index 99ced3c209..752f4bbb44 100644
--- a/frappe/event_streaming/doctype/event_update_log/test_event_update_log.py
+++ b/frappe/event_streaming/doctype/event_update_log/test_event_update_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.py b/frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.py
index 80a59e4c31..47180db74e 100644
--- a/frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.py
+++ b/frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/exceptions.py b/frappe/exceptions.py
index 13abd8f4f8..8449425bc1 100644
--- a/frappe/exceptions.py
+++ b/frappe/exceptions.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
# BEWARE don't put anything in this file except exceptions
from werkzeug.exceptions import NotFound
@@ -99,8 +99,10 @@ class IncompatibleApp(ValidationError): pass
class InvalidDates(ValidationError): pass
class DataTooLongException(ValidationError): pass
class FileAlreadyAttachedException(Exception): pass
-class DocumentAlreadyRestored(Exception): pass
-class AttachmentLimitReached(Exception): pass
+class DocumentAlreadyRestored(ValidationError): pass
+class AttachmentLimitReached(ValidationError): pass
+class QueryTimeoutError(Exception): pass
+class QueryDeadlockError(Exception): pass
# OAuth exceptions
class InvalidAuthorizationHeader(CSRFTokenError): pass
class InvalidAuthorizationPrefix(CSRFTokenError): pass
diff --git a/frappe/frappeclient.py b/frappe/frappeclient.py
index e57f82b60a..ab58979203 100644
--- a/frappe/frappeclient.py
+++ b/frappe/frappeclient.py
@@ -286,12 +286,16 @@ class FrappeClient(object):
doc.modified = frappe.db.get_single_value(doctype, "modified")
frappe.get_doc(doc).insert()
- def get_api(self, method, params={}):
+ def get_api(self, method, params=None):
+ if params is None:
+ params = {}
res = self.session.get(self.url + "/api/method/" + method + "/",
params=params, verify=self.verify, headers=self.headers)
return self.post_process(res)
- def post_api(self, method, params={}):
+ def post_api(self, method, params=None):
+ if params is None:
+ params = {}
res = self.session.post(self.url + "/api/method/" + method + "/",
params=params, verify=self.verify, headers=self.headers)
return self.post_process(res)
diff --git a/frappe/geo/country_info.py b/frappe/geo/country_info.py
index ddebd1fb0e..86f1d9bc2f 100644
--- a/frappe/geo/country_info.py
+++ b/frappe/geo/country_info.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
# all country info
import os, json, frappe
diff --git a/frappe/geo/doctype/country/country.py b/frappe/geo/doctype/country/country.py
index 54935e6eaf..a648744058 100644
--- a/frappe/geo/doctype/country/country.py
+++ b/frappe/geo/doctype/country/country.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/geo/doctype/country/test_country.py b/frappe/geo/doctype/country/test_country.py
index e00d6ecf37..b4d15f81b3 100644
--- a/frappe/geo/doctype/country/test_country.py
+++ b/frappe/geo/doctype/country/test_country.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: See license.txt
+# License: MIT. See LICENSE
import frappe
test_records = frappe.get_test_records('Country')
\ No newline at end of file
diff --git a/frappe/geo/doctype/currency/currency.py b/frappe/geo/doctype/currency/currency.py
index b3ce67cc67..fbe37e73bd 100644
--- a/frappe/geo/doctype/currency/currency.py
+++ b/frappe/geo/doctype/currency/currency.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import throw, _
diff --git a/frappe/geo/doctype/currency/test_currency.py b/frappe/geo/doctype/currency/test_currency.py
index 5552e675ec..71b963cc86 100644
--- a/frappe/geo/doctype/currency/test_currency.py
+++ b/frappe/geo/doctype/currency/test_currency.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: See license.txt
+# License: MIT. See LICENSE
# pre loaded
diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py
index 89de176f0b..9b44a2f3d8 100644
--- a/frappe/geo/utils.py
+++ b/frappe/geo/utils.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/handler.py b/frappe/handler.py
index de86c15c8f..42c17261b4 100755
--- a/frappe/handler.py
+++ b/frappe/handler.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
from werkzeug.wrappers import Response
@@ -10,6 +10,8 @@ from frappe.utils import cint
from frappe import _, is_whitelisted
from frappe.utils.response import build_response
from frappe.utils.csvutils import build_csv_response
+from frappe.utils.image import optimize_image
+from mimetypes import guess_type
from frappe.core.doctype.server_script.server_script_utils import run_server_script_api
@@ -25,7 +27,7 @@ def handle():
cmd = frappe.local.form_dict.cmd
data = None
- if cmd!='login':
+ if cmd != 'login':
data = execute_cmd(cmd)
# data can be an empty string or list which are valid responses
@@ -53,7 +55,7 @@ def execute_cmd(cmd, from_async=False):
try:
method = get_attr(cmd)
except Exception as e:
- frappe.throw(_('Invalid Method'))
+ frappe.throw(_('Failed to get method for command {0} with {1}').format(cmd, e))
if from_async:
method = method.queue
@@ -144,20 +146,32 @@ def upload_file():
file_url = frappe.form_dict.file_url
folder = frappe.form_dict.folder or 'Home'
method = frappe.form_dict.method
+ filename = frappe.form_dict.file_name
+ optimize = frappe.form_dict.optimize
content = None
- filename = None
if 'file' in files:
file = files['file']
content = file.stream.read()
filename = file.filename
+ content_type = guess_type(filename)[0]
+ if optimize and content_type.startswith("image/"):
+ args = {
+ "content": content,
+ "content_type": content_type
+ }
+ if frappe.form_dict.max_width:
+ args["max_width"] = int(frappe.form_dict.max_width)
+ if frappe.form_dict.max_height:
+ args["max_height"] = int(frappe.form_dict.max_height)
+ content = optimize_image(**args)
+
frappe.local.uploaded_file = content
frappe.local.uploaded_filename = filename
- if frappe.session.user == 'Guest' or (user and not user.has_desk_access()):
- import mimetypes
- filetype = mimetypes.guess_type(filename)[0]
+ if not file_url and (frappe.session.user == "Guest" or (user and not user.has_desk_access())):
+ filetype = guess_type(filename)[0]
if filetype not in ALLOWED_MIMETYPES:
frappe.throw(_("You can only upload JPG, PNG, PDF, or Microsoft documents."))
@@ -209,7 +223,10 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
doc = frappe.get_doc(dt, dn)
else:
- doc = frappe.get_doc(json.loads(docs))
+ if isinstance(docs, str):
+ docs = json.loads(docs)
+
+ doc = frappe.get_doc(docs)
doc._original_modified = doc.modified
doc.check_if_latest()
diff --git a/frappe/hooks.py b/frappe/hooks.py
index ac42a03461..8bca5c066c 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -12,11 +12,11 @@ source_link = "https://github.com/frappe/frappe"
app_license = "MIT"
app_logo_url = '/assets/frappe/images/frappe-framework-logo.svg'
-develop_version = '13.x.x-develop'
+develop_version = '14.x.x-develop'
-app_email = "info@frappe.io"
+app_email = "developers@frappe.io"
-docs_app = "frappe_io"
+docs_app = "frappe_docs"
translator_url = "https://translate.erpnext.com"
@@ -76,8 +76,6 @@ before_tests = "frappe.utils.install.before_tests"
email_append_to = ["Event", "ToDo", "Communication"]
-get_rooms = 'frappe.chat.doctype.chat_room.chat_room.get_rooms'
-
calendars = ["Event"]
leaderboards = "frappe.desk.leaderboard.get_leaderboards"
@@ -164,13 +162,17 @@ doc_events = {
"after_rename": "frappe.desk.notifications.clear_doctype_notifications",
"on_cancel": [
"frappe.desk.notifications.clear_doctype_notifications",
- "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions"
+ "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions",
+ "frappe.event_streaming.doctype.event_update_log.event_update_log.notify_consumers"
],
"on_trash": [
"frappe.desk.notifications.clear_doctype_notifications",
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions",
"frappe.event_streaming.doctype.event_update_log.event_update_log.notify_consumers"
],
+ "on_update_after_submit": [
+ "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions"
+ ],
"on_change": [
"frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points",
"frappe.automation.doctype.milestone_tracker.milestone_tracker.evaluate_milestone"
@@ -277,11 +279,6 @@ sounds = [
{"name": "error", "src": "/assets/frappe/sounds/error.mp3", "volume": 0.1},
{"name": "alert", "src": "/assets/frappe/sounds/alert.mp3", "volume": 0.2},
# {"name": "chime", "src": "/assets/frappe/sounds/chime.mp3"},
-
- # frappe.chat sounds
- { "name": "chat-message", "src": "/assets/frappe/sounds/chat-message.mp3", "volume": 0.1 },
- { "name": "chat-notification", "src": "/assets/frappe/sounds/chat-notification.mp3", "volume": 0.1 }
- # frappe.chat sounds
]
bot_parsers = [
diff --git a/frappe/installer.py b/frappe/installer.py
index acdb91e7cf..d1a13fdaab 100755
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -1,9 +1,11 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import json
import os
import sys
+from collections import OrderedDict
+from typing import List, Dict
import frappe
from frappe.defaults import _clear_cache
@@ -29,6 +31,10 @@ def _new_site(
):
"""Install a new Frappe site"""
+ from frappe.commands.scheduler import _is_scheduler_enabled
+ from frappe.utils import get_site_path, scheduler, touch_file
+
+
if not force and os.path.exists(site):
print("Site {0} already exists".format(site))
sys.exit(1)
@@ -37,14 +43,11 @@ def _new_site(
print("--no-mariadb-socket requires db_type to be set to mariadb.")
sys.exit(1)
- if not db_name:
- import hashlib
- db_name = "_" + hashlib.sha1(site.encode()).hexdigest()[:16]
-
frappe.init(site=site)
- from frappe.commands.scheduler import _is_scheduler_enabled
- from frappe.utils import get_site_path, scheduler, touch_file
+ if not db_name:
+ import hashlib
+ db_name = "_" + hashlib.sha1(os.path.realpath(frappe.get_site_path()).encode()).hexdigest()[:16]
try:
# enable scheduler post install?
@@ -157,7 +160,7 @@ def install_app(name, verbose=False, set_as_patched=True):
if name != "frappe":
add_module_defs(name)
- sync_for(name, force=True, sync_everything=True, verbose=verbose, reset_permissions=True)
+ sync_for(name, force=True, reset_permissions=True)
add_to_installed_apps(name)
@@ -229,49 +232,11 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
scheduled_backup(ignore_files=True)
frappe.flags.in_uninstall = True
- drop_doctypes = []
modules = frappe.get_all("Module Def", filters={"app_name": app_name}, pluck="name")
- for module_name in modules:
- print(f"Deleting Module '{module_name}'")
- for doctype in frappe.get_all(
- "DocType", filters={"module": module_name}, fields=["name", "issingle"]
- ):
- print(f"* removing DocType '{doctype.name}'...")
-
- if not dry_run:
- frappe.delete_doc("DocType", doctype.name, ignore_on_trash=True)
-
- if not doctype.issingle:
- drop_doctypes.append(doctype.name)
-
- linked_doctypes = frappe.get_all(
- "DocField", filters={"fieldtype": "Link", "options": "Module Def"}, fields=["parent"]
- )
- ordered_doctypes = ["Workspace", "Report", "Page", "Web Form"]
- all_doctypes_with_linked_modules = ordered_doctypes + [
- doctype.parent
- for doctype in linked_doctypes
- if doctype.parent not in ordered_doctypes
- ]
- doctypes_with_linked_modules = [
- x for x in all_doctypes_with_linked_modules if frappe.db.exists("DocType", x)
- ]
- for doctype in doctypes_with_linked_modules:
- for record in frappe.get_all(doctype, filters={"module": module_name}, pluck="name"):
- print(f"* removing {doctype} '{record}'...")
- if not dry_run:
- frappe.delete_doc(doctype, record, ignore_on_trash=True, force=True)
-
- print(f"* removing Module Def '{module_name}'...")
- if not dry_run:
- frappe.delete_doc("Module Def", module_name, ignore_on_trash=True, force=True)
-
- for doctype in set(drop_doctypes):
- print(f"* dropping Table for '{doctype}'...")
- if not dry_run:
- frappe.db.sql_ddl(f"drop table `tab{doctype}`")
+ drop_doctypes = _delete_modules(modules, dry_run=dry_run)
+ _delete_doctypes(drop_doctypes, dry_run=dry_run)
if not dry_run:
remove_from_installed_apps(app_name)
@@ -281,11 +246,91 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
frappe.flags.in_uninstall = False
+def _delete_modules(modules: List[str], dry_run: bool) -> List[str]:
+ """ Delete modules belonging to the app and all related doctypes.
+
+ Note: All record linked linked to Module Def are also deleted.
+
+ Returns: list of deleted doctypes."""
+ drop_doctypes = []
+
+ doctype_link_field_map = _get_module_linked_doctype_field_map()
+ for module_name in modules:
+ print(f"Deleting Module '{module_name}'")
+
+ for doctype in frappe.get_all(
+ "DocType", filters={"module": module_name}, fields=["name", "issingle"]
+ ):
+ print(f"* removing DocType '{doctype.name}'...")
+
+ if not dry_run:
+ if doctype.issingle:
+ frappe.delete_doc("DocType", doctype.name, ignore_on_trash=True)
+ else:
+ drop_doctypes.append(doctype.name)
+
+ _delete_linked_documents(module_name, doctype_link_field_map, dry_run=dry_run)
+
+ print(f"* removing Module Def '{module_name}'...")
+ if not dry_run:
+ frappe.delete_doc("Module Def", module_name, ignore_on_trash=True, force=True)
+
+ return drop_doctypes
+
+
+def _delete_linked_documents(
+ module_name: str,
+ doctype_linkfield_map: Dict[str, str],
+ dry_run: bool
+ ) -> None:
+
+ """Deleted all records linked with module def"""
+ for doctype, fieldname in doctype_linkfield_map.items():
+ for record in frappe.get_all(doctype, filters={fieldname: module_name}, pluck="name"):
+ print(f"* removing {doctype} '{record}'...")
+ if not dry_run:
+ frappe.delete_doc(doctype, record, ignore_on_trash=True, force=True)
+
+def _get_module_linked_doctype_field_map() -> Dict[str, str]:
+ """ Get all the doctypes which have module linked with them.
+
+ returns ordered dictionary with doctype->link field mapping."""
+
+ # Hardcoded to change order of deletion
+ ordered_doctypes = [
+ ("Workspace", "module"),
+ ("Report", "module"),
+ ("Page", "module"),
+ ("Web Form", "module")
+ ]
+ doctype_to_field_map = OrderedDict(ordered_doctypes)
+
+ linked_doctypes = frappe.get_all(
+ "DocField", filters={"fieldtype": "Link", "options": "Module Def"}, fields=["parent", "fieldname"]
+ )
+ existing_linked_doctypes = [d for d in linked_doctypes if frappe.db.exists("DocType", d.parent)]
+
+ for d in existing_linked_doctypes:
+ # DocType deletion is handled separately in the end
+ if d.parent not in doctype_to_field_map and d.parent != "DocType":
+ doctype_to_field_map[d.parent] = d.fieldname
+
+ return doctype_to_field_map
+
+
+def _delete_doctypes(doctypes: List[str], dry_run: bool) -> None:
+ for doctype in set(doctypes):
+ print(f"* dropping Table for '{doctype}'...")
+ if not dry_run:
+ frappe.delete_doc("DocType", doctype, ignore_on_trash=True)
+ frappe.db.sql_ddl(f"drop table `tab{doctype}`")
+
+
def post_install(rebuild_website=False):
- from frappe.website import render
+ from frappe.website.utils import clear_website_cache
if rebuild_website:
- render.clear_cache()
+ clear_website_cache()
init_singles()
frappe.db.commit()
@@ -445,9 +490,32 @@ def extract_sql_from_archive(sql_file_path):
else:
decompressed_file_name = sql_file_path
+ # convert archive sql to latest compatible
+ convert_archive_content(decompressed_file_name)
+
return decompressed_file_name
+def convert_archive_content(sql_file_path):
+ if frappe.conf.db_type == "mariadb":
+ # ever since mariaDB 10.6, row_format COMPRESSED has been deprecated and removed
+ # this step is added to ease restoring sites depending on older mariaDB servers
+ from frappe.utils import random_string
+ from pathlib import Path
+
+ old_sql_file_path = Path(f"{sql_file_path}_{random_string(10)}")
+ sql_file_path = Path(sql_file_path)
+
+ os.rename(sql_file_path, old_sql_file_path)
+ sql_file_path.touch()
+
+ with open(old_sql_file_path) as r, open(sql_file_path, "a") as w:
+ for line in r:
+ w.write(line.replace("ROW_FORMAT=COMPRESSED", "ROW_FORMAT=DYNAMIC"))
+
+ old_sql_file_path.unlink()
+
+
def extract_sql_gzip(sql_gz_path):
import subprocess
@@ -457,7 +525,7 @@ def extract_sql_gzip(sql_gz_path):
decompressed_file = original_file.rstrip(".gz")
cmd = 'gzip -dvf < {0} > {1}'.format(original_file, decompressed_file)
subprocess.check_call(cmd, shell=True)
- except:
+ except Exception:
raise
return decompressed_file
diff --git a/frappe/integrations/doctype/braintree_settings/braintree_settings.py b/frappe/integrations/doctype/braintree_settings/braintree_settings.py
index 9dc9778bee..59751185b9 100644
--- a/frappe/integrations/doctype/braintree_settings/braintree_settings.py
+++ b/frappe/integrations/doctype/braintree_settings/braintree_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/integrations/doctype/braintree_settings/test_braintree_settings.py b/frappe/integrations/doctype/braintree_settings/test_braintree_settings.py
index 72a678a92c..721158fb4a 100644
--- a/frappe/integrations/doctype/braintree_settings/test_braintree_settings.py
+++ b/frappe/integrations/doctype/braintree_settings/test_braintree_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import unittest
class TestBraintreeSettings(unittest.TestCase):
diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py
index 449e30f6d0..fcb5fe7ee9 100644
--- a/frappe/integrations/doctype/connected_app/connected_app.py
+++ b/frappe/integrations/doctype/connected_app/connected_app.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import os
from urllib.parse import urljoin
diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py
index d1ff19ecb2..eff7104ce0 100644
--- a/frappe/integrations/doctype/connected_app/test_connected_app.py
+++ b/frappe/integrations/doctype/connected_app/test_connected_app.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# See license.txt
+# License: MIT. See LICENSE
import unittest
import requests
from urllib.parse import urljoin
diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
index 53f0935c80..9ccd1c0210 100644
--- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
+++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import json
import os
@@ -336,7 +336,6 @@ def dropbox_auth_finish(return_access_token=False):
_("Dropbox access is approved!") + close,
indicator_color='green')
-@frappe.whitelist(allow_guest=True)
def set_dropbox_access_token(access_token):
frappe.db.set_value("Dropbox Settings", None, 'dropbox_access_token', access_token)
frappe.db.commit()
diff --git a/frappe/integrations/doctype/dropbox_settings/test_dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/test_dropbox_settings.py
index d34e65de50..458f876444 100644
--- a/frappe/integrations/doctype/dropbox_settings/test_dropbox_settings.py
+++ b/frappe/integrations/doctype/dropbox_settings/test_dropbox_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/integrations/doctype/google_calendar/google_calendar.py b/frappe/integrations/doctype/google_calendar/google_calendar.py
index f93be35aa7..0d4c5bbe5c 100644
--- a/frappe/integrations/doctype/google_calendar/google_calendar.py
+++ b/frappe/integrations/doctype/google_calendar/google_calendar.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from datetime import datetime, timedelta
diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py
index 1705f98e91..a63b0b6d80 100644
--- a/frappe/integrations/doctype/google_contacts/google_contacts.py
+++ b/frappe/integrations/doctype/google_contacts/google_contacts.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import google.oauth2.credentials
diff --git a/frappe/integrations/doctype/google_drive/google_drive.py b/frappe/integrations/doctype/google_drive/google_drive.py
index 93b6fa3f8d..beac7898a9 100644
--- a/frappe/integrations/doctype/google_drive/google_drive.py
+++ b/frappe/integrations/doctype/google_drive/google_drive.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import os
from urllib.parse import quote
diff --git a/frappe/integrations/doctype/google_drive/test_google_drive.py b/frappe/integrations/doctype/google_drive/test_google_drive.py
index 96e8577c7c..fbd9dce7f4 100644
--- a/frappe/integrations/doctype/google_drive/test_google_drive.py
+++ b/frappe/integrations/doctype/google_drive/test_google_drive.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/integrations/doctype/google_settings/google_settings.json b/frappe/integrations/doctype/google_settings/google_settings.json
index 086c56c020..6f25fa4bf6 100644
--- a/frappe/integrations/doctype/google_settings/google_settings.json
+++ b/frappe/integrations/doctype/google_settings/google_settings.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2019-06-14 00:08:37.255003",
"doctype": "DocType",
"engine": "InnoDB",
@@ -8,7 +9,10 @@
"client_id",
"client_secret",
"sb_01",
- "api_key"
+ "api_key",
+ "section_break_7",
+ "google_drive_picker_enabled",
+ "app_id"
],
"fields": [
{
@@ -18,10 +22,12 @@
"label": "Enable"
},
{
+ "description": "The Client ID obtained from the Google Cloud Console under \n\"APIs & Services\" > \"Credentials\"\n",
"fieldname": "client_id",
"fieldtype": "Data",
"in_list_view": 1,
- "label": "Client ID"
+ "label": "Client ID",
+ "mandatory_depends_on": "google_drive_picker_enabled"
},
{
"fieldname": "client_secret",
@@ -30,10 +36,11 @@
"label": "Client Secret"
},
{
- "description": "Used For Google Maps Integration.",
+ "description": "The browser API key obtained from the Google Cloud Console under \n\"APIs & Services\" > \"Credentials\"\n",
"fieldname": "api_key",
"fieldtype": "Data",
- "label": "API Key"
+ "label": "API Key",
+ "mandatory_depends_on": "google_drive_picker_enabled"
},
{
"depends_on": "enable",
@@ -46,10 +53,30 @@
"fieldname": "sb_01",
"fieldtype": "Section Break",
"label": "API Key"
+ },
+ {
+ "depends_on": "google_drive_picker_enabled",
+ "description": "The project number obtained from Google Cloud Console under \n\"IAM & Admin\" > \"Settings\"\n",
+ "fieldname": "app_id",
+ "fieldtype": "Data",
+ "label": "App ID",
+ "mandatory_depends_on": "google_drive_picker_enabled"
+ },
+ {
+ "fieldname": "section_break_7",
+ "fieldtype": "Section Break",
+ "label": "Google Drive Picker"
+ },
+ {
+ "default": "0",
+ "fieldname": "google_drive_picker_enabled",
+ "fieldtype": "Check",
+ "label": "Google Drive Picker Enabled"
}
],
"issingle": 1,
- "modified": "2019-08-06 22:37:41.699703",
+ "links": [],
+ "modified": "2021-06-29 18:26:07.094851",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Google Settings",
@@ -64,16 +91,6 @@
"role": "System Manager",
"share": 1,
"write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "All",
- "share": 1,
- "write": 1
}
],
"quick_entry": 1,
diff --git a/frappe/integrations/doctype/google_settings/google_settings.py b/frappe/integrations/doctype/google_settings/google_settings.py
index 9a3f3c8ae2..94df43e69c 100644
--- a/frappe/integrations/doctype/google_settings/google_settings.py
+++ b/frappe/integrations/doctype/google_settings/google_settings.py
@@ -1,12 +1,27 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
-# import frappe
+import frappe
from frappe.model.document import Document
class GoogleSettings(Document):
pass
def get_auth_url():
- return "https://www.googleapis.com/oauth2/v4/token"
\ No newline at end of file
+ return "https://www.googleapis.com/oauth2/v4/token"
+
+
+@frappe.whitelist()
+def get_file_picker_settings():
+ """Return all the data FileUploader needs to start the Google Drive Picker."""
+ google_settings = frappe.get_single("Google Settings")
+ if not (google_settings.enable and google_settings.google_drive_picker_enabled):
+ return {}
+
+ return {
+ "enabled": True,
+ "appId": google_settings.app_id,
+ "developerKey": google_settings.api_key,
+ "clientId": google_settings.client_id
+ }
diff --git a/frappe/integrations/doctype/google_settings/test_google_settings.py b/frappe/integrations/doctype/google_settings/test_google_settings.py
new file mode 100644
index 0000000000..cddf9f3697
--- /dev/null
+++ b/frappe/integrations/doctype/google_settings/test_google_settings.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# License: MIT. See LICENSE
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+
+from .google_settings import get_file_picker_settings
+
+class TestGoogleSettings(unittest.TestCase):
+
+ def setUp(self):
+ settings = frappe.get_single("Google Settings")
+ settings.client_id = "test_client_id"
+ settings.app_id = "test_app_id"
+ settings.api_key = "test_api_key"
+ settings.save()
+
+ def test_picker_disabled(self):
+ """Google Drive Picker should be disabled if it is not enabled in Google Settings."""
+ frappe.db.set_value("Google Settings", None, "enable", 1)
+ frappe.db.set_value("Google Settings", None, "google_drive_picker_enabled", 0)
+ settings = get_file_picker_settings()
+
+ self.assertEqual(settings, {})
+
+ def test_google_disabled(self):
+ """Google Drive Picker should be disabled if Google integration is not enabled."""
+ frappe.db.set_value("Google Settings", None, "enable", 0)
+ frappe.db.set_value("Google Settings", None, "google_drive_picker_enabled", 1)
+ settings = get_file_picker_settings()
+
+ self.assertEqual(settings, {})
+
+ def test_picker_enabled(self):
+ """If picker is enabled, get_file_picker_settings should return the credentials."""
+ frappe.db.set_value("Google Settings", None, "enable", 1)
+ frappe.db.set_value("Google Settings", None, "google_drive_picker_enabled", 1)
+ settings = get_file_picker_settings()
+
+ self.assertEqual(True, settings.get("enabled", False))
+ self.assertEqual("test_client_id", settings.get("clientId", ""))
+ self.assertEqual("test_app_id", settings.get("appId", ""))
+ self.assertEqual("test_api_key", settings.get("developerKey", ""))
diff --git a/frappe/integrations/doctype/integration_request/integration_request.py b/frappe/integrations/doctype/integration_request/integration_request.py
index 4c4961d96d..ae0e024f58 100644
--- a/frappe/integrations/doctype/integration_request/integration_request.py
+++ b/frappe/integrations/doctype/integration_request/integration_request.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/integrations/doctype/integration_request/test_integration_request.py b/frappe/integrations/doctype/integration_request/test_integration_request.py
index a26eb4ba93..e26ccabc96 100644
--- a/frappe/integrations/doctype/integration_request/test_integration_request.py
+++ b/frappe/integrations/doctype/integration_request/test_integration_request.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py b/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py
index b6bb77d964..b9838b996f 100644
--- a/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py
+++ b/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.json b/frappe/integrations/doctype/ldap_settings/ldap_settings.json
index 5d30a873fb..d915ae2ad6 100644
--- a/frappe/integrations/doctype/ldap_settings/ldap_settings.json
+++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2016-09-22 04:16:48.829658",
"doctype": "DocType",
"document_type": "System",
@@ -6,18 +7,24 @@
"engine": "InnoDB",
"field_order": [
"enabled",
- "ldap_server_url",
+ "ldap_server_settings_section",
+ "ldap_directory_server",
"column_break_4",
+ "ldap_server_url",
+ "ldap_auth_section",
"base_dn",
+ "column_break_8",
"password",
- "section_break_5",
- "organizational_unit",
- "default_role",
+ "ldap_search_and_paths_section",
+ "ldap_search_path_user",
"ldap_search_string",
+ "column_break_12",
+ "ldap_search_path_group",
+ "ldap_user_creation_and_mapping_section",
"ldap_email_field",
"ldap_username_field",
- "column_break_11",
"ldap_first_name_field",
+ "column_break_19",
"ldap_middle_name_field",
"ldap_last_name_field",
"ldap_phone_field",
@@ -25,13 +32,18 @@
"ldap_security",
"ssl_tls_mode",
"require_trusted_certificate",
- "column_break_17",
+ "column_break_27",
"local_private_key_file",
"local_server_certificate_file",
"local_ca_certs_file",
+ "ldap_custom_settings_section",
+ "ldap_group_objectclass",
+ "column_break_33",
+ "ldap_group_member_attribute",
"ldap_group_mappings_section",
- "ldap_group_field",
- "ldap_groups"
+ "default_role",
+ "ldap_groups",
+ "ldap_group_field"
],
"fields": [
{
@@ -65,18 +77,6 @@
"label": "Password for Base DN",
"reqd": 1
},
- {
- "fieldname": "section_break_5",
- "fieldtype": "Section Break",
- "label": "LDAP User Creation and Mapping"
- },
- {
- "fieldname": "organizational_unit",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Organizational Unit for Users",
- "reqd": 1
- },
{
"fieldname": "default_role",
"fieldtype": "Link",
@@ -85,6 +85,7 @@
"reqd": 1
},
{
+ "description": "Must be enclosed in '()' and include '{0}', which is a placeholder for the user/login name. i.e. (&(objectclass=user)(uid={0}))",
"fieldname": "ldap_search_string",
"fieldtype": "Data",
"label": "LDAP Search String",
@@ -102,10 +103,6 @@
"label": "LDAP Username Field",
"reqd": 1
},
- {
- "fieldname": "column_break_11",
- "fieldtype": "Column Break"
- },
{
"fieldname": "ldap_first_name_field",
"fieldtype": "Data",
@@ -152,10 +149,6 @@
"options": "No\nYes",
"reqd": 1
},
- {
- "fieldname": "column_break_17",
- "fieldtype": "Column Break"
- },
{
"fieldname": "local_private_key_file",
"fieldtype": "Data",
@@ -177,6 +170,7 @@
"label": "LDAP Group Mappings"
},
{
+ "description": "NOTE: This box is due for depreciation. Please re-setup LDAP to work with the newer settings",
"fieldname": "ldap_group_field",
"fieldtype": "Data",
"label": "LDAP Group Field"
@@ -186,11 +180,93 @@
"fieldtype": "Table",
"label": "LDAP Group Mappings",
"options": "LDAP Group Mapping"
+ },
+ {
+ "fieldname": "ldap_server_settings_section",
+ "fieldtype": "Section Break",
+ "label": "LDAP Server Settings"
+ },
+ {
+ "fieldname": "ldap_auth_section",
+ "fieldtype": "Section Break",
+ "label": "LDAP Auth"
+ },
+ {
+ "fieldname": "column_break_8",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "ldap_search_and_paths_section",
+ "fieldtype": "Section Break",
+ "label": "LDAP Search and Paths"
+ },
+ {
+ "fieldname": "column_break_12",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "ldap_user_creation_and_mapping_section",
+ "fieldtype": "Section Break",
+ "label": "LDAP User Creation and Mapping"
+ },
+ {
+ "fieldname": "column_break_19",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_27",
+ "fieldtype": "Column Break"
+ },
+ {
+ "description": "These settings are required if 'Custom' LDAP Directory is used",
+ "fieldname": "ldap_custom_settings_section",
+ "fieldtype": "Section Break",
+ "label": "LDAP Custom Settings"
+ },
+ {
+ "fieldname": "column_break_33",
+ "fieldtype": "Column Break"
+ },
+ {
+ "description": "string value, i.e. member",
+ "fieldname": "ldap_group_member_attribute",
+ "fieldtype": "Data",
+ "label": "LDAP Group Member attribute"
+ },
+ {
+ "description": "Please select the LDAP Directory being used",
+ "fieldname": "ldap_directory_server",
+ "fieldtype": "Select",
+ "label": "Directory Server",
+ "options": "\nActive Directory\nOpenLDAP\nCustom",
+ "reqd": 1
+ },
+ {
+ "description": "string value, i.e. group",
+ "fieldname": "ldap_group_objectclass",
+ "fieldtype": "Data",
+ "label": "Group Object Class"
+ },
+ {
+ "description": "Requires any valid fdn path. i.e. ou=users,dc=example,dc=com",
+ "fieldname": "ldap_search_path_user",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "LDAP search path for Users",
+ "reqd": 1
+ },
+ {
+ "description": "Requires any valid fdn path. i.e. ou=groups,dc=example,dc=com",
+ "fieldname": "ldap_search_path_group",
+ "fieldtype": "Data",
+ "label": "LDAP search path for Groups",
+ "reqd": 1
}
],
"in_create": 1,
"issingle": 1,
- "modified": "2019-07-15 06:48:16.562109",
+ "links": [],
+ "modified": "2021-07-27 11:51:43.328271",
"modified_by": "Administrator",
"module": "Integrations",
"name": "LDAP Settings",
diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py
index acc8b96679..1c5abb454c 100644
--- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py
+++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _, safe_encode
@@ -13,10 +13,44 @@ class LDAPSettings(Document):
return
if not self.flags.ignore_mandatory:
- if self.ldap_search_string and self.ldap_search_string.endswith("={0}"):
- self.connect_to_ldap(base_dn=self.base_dn, password=self.get_password(raise_exception=False))
+
+ if self.ldap_search_string.count('(') == self.ldap_search_string.count(')') and \
+ self.ldap_search_string.startswith('(') and \
+ self.ldap_search_string.endswith(')') and \
+ self.ldap_search_string and \
+ "{0}" in self.ldap_search_string:
+
+ conn = self.connect_to_ldap(base_dn=self.base_dn, password=self.get_password(raise_exception=False))
+
+ try:
+ if conn.result['type'] == 'bindResponse' and self.base_dn:
+ import ldap3
+
+ conn.search(
+ search_base=self.ldap_search_path_user,
+ search_filter="(objectClass=*)",
+ attributes=self.get_ldap_attributes())
+
+ conn.search(
+ search_base=self.ldap_search_path_group,
+ search_filter="(objectClass=*)",
+ attributes=['cn'])
+
+ except ldap3.core.exceptions.LDAPAttributeError as ex:
+ frappe.throw(_("LDAP settings incorrect. validation response was: {0}").format(ex),
+ title=_("Misconfigured"))
+
+ except ldap3.core.exceptions.LDAPNoSuchObjectResult:
+ frappe.throw(_("Ensure the user and group search paths are correct."),
+ title=_("Misconfigured"))
+
+ if self.ldap_directory_server.lower() == 'custom':
+ if not self.ldap_group_member_attribute or not self.ldap_group_mappings_section:
+ frappe.throw(_("Custom LDAP Directoy Selected, please ensure 'LDAP Group Member attribute' and 'LDAP Group Mappings' are entered"),
+ title=_("Misconfigured"))
+
else:
- frappe.throw(_("LDAP Search String needs to end with a placeholder, eg sAMAccountName={0}"))
+ frappe.throw(_("LDAP Search String must be enclosed in '()' and needs to contian the user placeholder {0}, eg sAMAccountName={0}"))
def connect_to_ldap(self, base_dn, password, read_only=True):
try:
@@ -118,8 +152,8 @@ class LDAPSettings(Document):
user.insert(ignore_permissions=True)
# always add default role.
user.add_roles(self.default_role)
- if self.ldap_group_field:
- self.sync_roles(user, groups)
+ self.sync_roles(user, groups)
+
return user
def get_ldap_attributes(self):
@@ -142,6 +176,66 @@ class LDAPSettings(Document):
return ldap_attributes
+
+ def fetch_ldap_groups(self, user, conn):
+ import ldap3
+
+ if type(user) is not ldap3.abstract.entry.Entry:
+ raise TypeError("Invalid type, attribute {0} must be of type '{1}'".format('user', 'ldap3.abstract.entry.Entry'))
+
+ if type(conn) is not ldap3.core.connection.Connection:
+ raise TypeError("Invalid type, attribute {0} must be of type '{1}'".format('conn', 'ldap3.Connection'))
+
+ fetch_ldap_groups = None
+
+ ldap_object_class = None
+ ldap_group_members_attribute = None
+
+
+ if self.ldap_directory_server.lower() == 'active directory':
+
+ ldap_object_class = 'Group'
+ ldap_group_members_attribute = 'member'
+ user_search_str = user.entry_dn
+
+
+ elif self.ldap_directory_server.lower() == 'openldap':
+
+ ldap_object_class = 'posixgroup'
+ ldap_group_members_attribute = 'memberuid'
+ user_search_str = getattr(user, self.ldap_username_field).value
+
+ elif self.ldap_directory_server.lower() == 'custom':
+
+ ldap_object_class = self.ldap_group_objectclass
+ ldap_group_members_attribute = self.ldap_group_member_attribute
+ user_search_str = getattr(user, self.ldap_username_field).value
+
+ else:
+ # NOTE: depreciate this else path
+ # this path will be hit for everyone with preconfigured ldap settings. this must be taken into account so as not to break ldap for those users.
+
+ if self.ldap_group_field:
+
+ fetch_ldap_groups = getattr(user, self.ldap_group_field).values
+
+ if ldap_object_class is not None:
+ conn.search(
+ search_base=self.ldap_search_path_group,
+ search_filter="(&(objectClass={0})({1}={2}))".format(ldap_object_class,ldap_group_members_attribute, user_search_str),
+ attributes=['cn']) # Build search query
+
+ if len(conn.entries) >= 1:
+
+ fetch_ldap_groups = []
+ for group in conn.entries:
+ fetch_ldap_groups.append(group['cn'].value)
+
+ return fetch_ldap_groups
+
+
+
+
def authenticate(self, username, password):
if not self.enabled:
@@ -152,23 +246,33 @@ class LDAPSettings(Document):
conn = self.connect_to_ldap(self.base_dn, self.get_password(raise_exception=False))
- conn.search(
- search_base=self.organizational_unit,
- search_filter="({0})".format(user_filter),
- attributes=ldap_attributes)
+ try:
+ import ldap3
- if len(conn.entries) == 1 and conn.entries[0]:
- user = conn.entries[0]
- # only try and connect as the user, once we have their fqdn entry.
- self.connect_to_ldap(base_dn=user.entry_dn, password=password)
+ conn.search(
+ search_base=self.ldap_search_path_user,
+ search_filter="{0}".format(user_filter),
+ attributes=ldap_attributes)
- groups = None
- if self.ldap_group_field:
- groups = getattr(user, self.ldap_group_field).values
- return self.create_or_update_user(self.convert_ldap_entry_to_dict(user), groups=groups)
- else:
+ if len(conn.entries) == 1 and conn.entries[0]:
+ user = conn.entries[0]
+
+ groups = self.fetch_ldap_groups(user, conn)
+
+ # only try and connect as the user, once we have their fqdn entry.
+ if user.entry_dn and password and conn.rebind(user=user.entry_dn, password=password):
+
+ return self.create_or_update_user(self.convert_ldap_entry_to_dict(user), groups=groups)
+
+ raise ldap3.core.exceptions.LDAPInvalidCredentialsResult # even though nothing foundor failed authentication raise invalid credentials
+
+ except ldap3.core.exceptions.LDAPInvalidFilterError:
+ frappe.throw(_("Please use a valid LDAP search filter"), title=_("Misconfigured"))
+
+ except ldap3.core.exceptions.LDAPInvalidCredentialsResult:
frappe.throw(_("Invalid username or password"))
+
def reset_password(self, user, password, logout_sessions=False):
from ldap3 import HASHED_SALTED_SHA, MODIFY_REPLACE
from ldap3.utils.hashed import hashed
@@ -179,7 +283,7 @@ class LDAPSettings(Document):
read_only=False)
if conn.search(
- search_base=self.organizational_unit,
+ search_base=self.ldap_search_path_user,
search_filter=search_filter,
attributes=self.get_ldap_attributes()
):
diff --git a/frappe/integrations/doctype/ldap_settings/test_data_ldif_activedirectory.json b/frappe/integrations/doctype/ldap_settings/test_data_ldif_activedirectory.json
new file mode 100644
index 0000000000..9777452af8
--- /dev/null
+++ b/frappe/integrations/doctype/ldap_settings/test_data_ldif_activedirectory.json
@@ -0,0 +1,338 @@
+{
+ "entries": [
+ {
+ "attributes": {
+ "cn": "base_dn_user",
+ "memberOf": [
+ "cn=Domain Users,ou=Groups,dc=unit,dc=testing",
+ "cn=Enterprise Administrators,ou=Groups,dc=unit,dc=testing"
+ ],
+ "objectClass": [
+ "user",
+ "top",
+ "person",
+ "organizationalPerson"
+ ],
+ "samaccountname": "cn=base_dn_user,dc=unit,dc=testing",
+ "sn": "user_sn",
+ "userPassword": [
+ "my_password"
+ ]
+ },
+ "dn": "cn=base_dn_user,dc=unit,dc=testing",
+ "raw": {
+ "cn": [
+ "base_dn_user"
+ ],
+ "memberOf": [
+ "cn=Domain Users,ou=Groups,dc=unit,dc=testing",
+ "cn=Enterprise Administrators,ou=Groups,dc=unit,dc=testing"
+ ],
+ "objectClass": [
+ "user",
+ "top",
+ "person",
+ "organizationalPerson"
+ ],
+ "samaccountname": [
+ "cn=base_dn_user,dc=unit,dc=testing"
+ ],
+ "sn": [
+ "user_sn"
+ ],
+ "userPassword": [
+ "my_password"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "cn": "Posix User1",
+ "description": [
+ "ACCESS:test1,ACCESS:test2"
+ ],
+ "givenname": "Posix",
+ "mail": "posix.user1@unit.testing",
+ "memberOf": [
+ "cn=Domain Users,ou=Groups,dc=unit,dc=testing",
+ "cn=Domain Administrators,ou=Groups,dc=unit,dc=testing"
+ ],
+ "mobile": "0421 123 456",
+ "objectClass": [
+ "user",
+ "top",
+ "person",
+ "organizationalPerson"
+ ],
+ "samaccountname": "posix.user",
+ "sn": "User1",
+ "telephonenumber": "08 8912 3456",
+ "userpassword": [
+ "posix_user_password"
+ ]
+ },
+ "dn": "cn=Posix User1,ou=Users,dc=unit,dc=testing",
+ "raw": {
+ "cn": [
+ "Posix User1"
+ ],
+ "description": [
+ "ACCESS:test1,ACCESS:test2"
+ ],
+ "givenname": [
+ "Posix"
+ ],
+ "mail": [
+ "posix.user1@unit.testing"
+ ],
+ "memberOf": [
+ "cn=Domain Users,ou=Groups,dc=unit,dc=testing",
+ "cn=Domain Administrators,ou=Groups,dc=unit,dc=testing"
+ ],
+ "mobile": [
+ "0421 123 456"
+ ],
+ "objectClass": [
+ "user",
+ "top",
+ "person",
+ "organizationalPerson"
+ ],
+ "samaccountname": [
+ "posix.user"
+ ],
+ "sn": [
+ "User1"
+ ],
+ "telephonenumber": [
+ "08 8912 3456"
+ ],
+ "userpassword": [
+ "posix_user_password"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "cn": "Posix User2",
+ "description": [
+ "ACCESS:test1,ACCESS:test3"
+ ],
+ "givenname": "Posix",
+ "homedirectory": "/home/users/posix.user2",
+ "mail": "posix.user2@unit.testing",
+ "memberOf": [
+ "cn=Domain Users,ou=Groups,dc=unit,dc=testing",
+ "cn=Enterprise Administrators,ou=Groups,dc=unit,dc=testing"
+ ],
+ "mobile": "0421 456 789",
+ "objectClass": [
+ "user",
+ "top",
+ "person",
+ "organizationalPerson"
+ ],
+ "samaccountname": "posix.user2",
+ "sn": "User2",
+ "telephonenumber": "08 8978 1234",
+ "userpassword": [
+ "posix_user2_password"
+ ]
+ },
+ "dn": "cn=Posix User2,ou=Users,dc=unit,dc=testing",
+ "raw": {
+ "cn": [
+ "Posix User2"
+ ],
+ "description": [
+ "ACCESS:test1,ACCESS:test3"
+ ],
+ "givenname": [
+ "Posix"
+ ],
+ "homedirectory": [
+ "/home/users/posix.user2"
+ ],
+ "mail": [
+ "posix.user2@unit.testing"
+ ],
+ "memberOf": [
+ "cn=Domain Users,ou=Groups,dc=unit,dc=testing",
+ "cn=Enterprise Administrators,ou=Groups,dc=unit,dc=testing"
+ ],
+ "mobile": [
+ "0421 456 789"
+ ],
+ "objectClass": [
+ "user",
+ "top",
+ "person",
+ "organizationalPerson"
+ ],
+ "samaccountname": [
+ "posix.user2"
+ ],
+ "sn": [
+ "User2"
+ ],
+ "telephonenumber": [
+ "08 8978 1234"
+ ],
+ "userpassword": [
+ "posix_user2_password"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "objectClass": [
+ "top",
+ "organizationalUnit"
+ ],
+ "ou": [
+ "Users"
+ ]
+ },
+ "dn": "ou=Users,dc=unit,dc=testing",
+ "raw": {
+ "objectClass": [
+ "top",
+ "organizationalUnit"
+ ],
+ "ou": [
+ "Users"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "Member": [
+ "cn=Posix User2,ou=Users,dc=unit,dc=testing"
+ ],
+ "cn": "Enterprise Administrators",
+ "description": [
+ "group contains only posix.user2"
+ ],
+ "groupType": 2147483652,
+ "objectClass": [
+ "top",
+ "group"
+ ]
+ },
+ "dn": "cn=Enterprise Administrators,ou=Groups,dc=unit,dc=testing",
+ "raw": {
+ "Member": [
+ "cn=Posix User2,ou=Users,dc=unit,dc=testing"
+ ],
+ "cn": [
+ "Enterprise Administrators"
+ ],
+ "description": [
+ "group contains only posix.user2"
+ ],
+ "groupType": [
+ "2147483652"
+ ],
+ "objectClass": [
+ "top",
+ "group"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "Member": [
+ "cn=Posix User1,ou=Users,dc=unit,dc=testing",
+ "cn=Posix User2,ou=Users,dc=unit,dc=testing"
+ ],
+ "cn": "Domain Users",
+ "description": [
+ "group2 Users contains only posix.user and posix.user2"
+ ],
+ "groupType": 2147483652,
+ "objectClass": [
+ "top",
+ "group"
+ ]
+ },
+ "dn": "cn=Domain Users,ou=Groups,dc=unit,dc=testing",
+ "raw": {
+ "Member": [
+ "cn=Posix User1,ou=Users,dc=unit,dc=testing",
+ "cn=Posix User2,ou=Users,dc=unit,dc=testing"
+ ],
+ "cn": [
+ "Domain Users"
+ ],
+ "description": [
+ "group2 Users contains only posix.user and posix.user2"
+ ],
+ "groupType": [
+ "2147483652"
+ ],
+ "objectClass": [
+ "top",
+ "group"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "Member": [
+ "cn=Posix User1,ou=Users,dc=unit,dc=testing",
+ "cn=base_dn_user,dc=unit,dc=testing"
+ ],
+ "cn": "Domain Administrators",
+ "description": [
+ "group1 Administrators contains only posix.user only"
+ ],
+ "groupType": 2147483652,
+ "objectClass": [
+ "top",
+ "group"
+ ]
+ },
+ "dn": "cn=Domain Administrators,ou=Groups,dc=unit,dc=testing",
+ "raw": {
+ "Member": [
+ "cn=Posix User1,ou=Users,dc=unit,dc=testing",
+ "cn=base_dn_user,dc=unit,dc=testing"
+ ],
+ "cn": [
+ "Domain Administrators"
+ ],
+ "description": [
+ "group1 Administrators contains only posix.user only"
+ ],
+ "groupType": [
+ "2147483652"
+ ],
+ "objectClass": [
+ "top",
+ "group"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "objectClass": [
+ "top",
+ "organizationalUnit"
+ ],
+ "ou": [
+ "Groups"
+ ]
+ },
+ "dn": "ou=Groups,dc=unit,dc=testing",
+ "raw": {
+ "objectClass": [
+ "top",
+ "organizationalUnit"
+ ],
+ "ou": [
+ "Groups"
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/frappe/integrations/doctype/ldap_settings/test_data_ldif_openldap.json b/frappe/integrations/doctype/ldap_settings/test_data_ldif_openldap.json
new file mode 100644
index 0000000000..86a76c1abc
--- /dev/null
+++ b/frappe/integrations/doctype/ldap_settings/test_data_ldif_openldap.json
@@ -0,0 +1,400 @@
+{
+ "entries": [
+ {
+ "attributes": {
+ "cn": [
+ "base_dn_user"
+ ],
+ "objectClass": [
+ "simpleSecurityObject",
+ "organizationalRole",
+ "top"
+ ],
+ "sn": [
+ "user_sn"
+ ],
+ "userPassword": [
+ "my_password"
+ ]
+ },
+ "dn": "cn=base_dn_user,dc=unit,dc=testing",
+ "raw": {
+ "cn": [
+ "base_dn_user"
+ ],
+ "objectClass": [
+ "simpleSecurityObject",
+ "organizationalRole",
+ "top"
+ ],
+ "sn": [
+ "user_sn"
+ ],
+ "userPassword": [
+ "my_password"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "cn": [
+ "Posix User2"
+ ],
+ "description": [
+ "ACCESS:test1,ACCESS:test3"
+ ],
+ "gidnumber": 501,
+ "givenname": [
+ "Posix2"
+ ],
+ "homedirectory": "/home/users/posix.user2",
+ "mail": [
+ "posix.user2@unit.testing"
+ ],
+ "mobile": [
+ "0421 456 789"
+ ],
+ "objectClass": [
+ "posixAccount",
+ "top",
+ "inetOrgPerson",
+ "person",
+ "organizationalPerson"
+ ],
+ "sn": [
+ "User2"
+ ],
+ "telephonenumber": [
+ "08 8978 1234"
+ ],
+ "uid": [
+ "posix.user2"
+ ],
+ "uidnumber": 1000,
+ "userpassword": [
+ "posix_user2_password"
+ ]
+ },
+ "dn": "cn=Posix User2,ou=users,dc=unit,dc=testing",
+ "raw": {
+ "cn": [
+ "Posix User2"
+ ],
+ "description": [
+ "ACCESS:test1,ACCESS:test3"
+ ],
+ "gidnumber": [
+ "501"
+ ],
+ "givenname": [
+ "Posix2"
+ ],
+ "homedirectory": [
+ "/home/users/posix.user2"
+ ],
+ "mail": [
+ "posix.user2@unit.testing"
+ ],
+ "mobile": [
+ "0421 456 789"
+ ],
+ "objectClass": [
+ "posixAccount",
+ "top",
+ "inetOrgPerson",
+ "person",
+ "organizationalPerson"
+ ],
+ "sn": [
+ "User2"
+ ],
+ "telephonenumber": [
+ "08 8978 1234"
+ ],
+ "uid": [
+ "posix.user2"
+ ],
+ "uidnumber": [
+ "1000"
+ ],
+ "userpassword": [
+ "posix_user2_password"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "cn": [
+ "Posix User1"
+ ],
+ "description": [
+ "ACCESS:test1,ACCESS:test2"
+ ],
+ "gidnumber": 501,
+ "givenname": [
+ "Posix"
+ ],
+ "homedirectory": "/home/users/posix.user",
+ "mail": [
+ "posix.user1@unit.testing"
+ ],
+ "mobile": [
+ "0421 123 456"
+ ],
+ "objectClass": [
+ "posixAccount",
+ "top",
+ "inetOrgPerson",
+ "person",
+ "organizationalPerson"
+ ],
+ "sn": [
+ "User1"
+ ],
+ "telephonenumber": [
+ "08 8912 3456"
+ ],
+ "uid": [
+ "posix.user"
+ ],
+ "uidnumber": 1000,
+ "userpassword": [
+ "posix_user_password"
+ ]
+ },
+ "dn": "cn=Posix User1,ou=users,dc=unit,dc=testing",
+ "raw": {
+ "cn": [
+ "Posix User1"
+ ],
+ "description": [
+ "ACCESS:test1,ACCESS:test2"
+ ],
+ "gidnumber": [
+ "501"
+ ],
+ "givenname": [
+ "Posix"
+ ],
+ "homedirectory": [
+ "/home/users/posix.user"
+ ],
+ "mail": [
+ "posix.user1@unit.testing"
+ ],
+ "mobile": [
+ "0421 123 456"
+ ],
+ "objectClass": [
+ "posixAccount",
+ "top",
+ "inetOrgPerson",
+ "person",
+ "organizationalPerson"
+ ],
+ "sn": [
+ "User1"
+ ],
+ "telephonenumber": [
+ "08 8912 3456"
+ ],
+ "uid": [
+ "posix.user"
+ ],
+ "uidnumber": [
+ "1000"
+ ],
+ "userpassword": [
+ "posix_user_password"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "objectClass": [
+ "top",
+ "organizationalUnit"
+ ],
+ "ou": [
+ "Users",
+ "users"
+ ]
+ },
+ "dn": "ou=users,dc=unit,dc=testing",
+ "raw": {
+ "objectClass": [
+ "top",
+ "organizationalUnit"
+ ],
+ "ou": [
+ "Users",
+ "users"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "dc": "testing",
+ "o": [
+ "Testing"
+ ],
+ "objectClass": [
+ "top",
+ "organization",
+ "dcObject"
+ ]
+ },
+ "dn": "dc=unit,dc=testing",
+ "raw": {
+ "dc": [
+ "testing",
+ "unit"
+ ],
+ "o": [
+ "Testing"
+ ],
+ "objectClass": [
+ "top",
+ "organization",
+ "dcObject"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "cn": [
+ "Users"
+ ],
+ "description": [
+ "group2 Users contains only posix.user and posix.user2"
+ ],
+ "gidnumber": 501,
+ "memberuid": [
+ "posix.user2",
+ "posix.user"
+ ],
+ "objectClass": [
+ "top",
+ "posixGroup"
+ ]
+ },
+ "dn": "cn=Users,ou=groups,dc=unit,dc=testing",
+ "raw": {
+ "cn": [
+ "Users"
+ ],
+ "description": [
+ "group2 Users contains only posix.user and posix.user2"
+ ],
+ "gidnumber": [
+ "501"
+ ],
+ "memberuid": [
+ "posix.user2",
+ "posix.user"
+ ],
+ "objectClass": [
+ "top",
+ "posixGroup"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "cn": [
+ "Administrators"
+ ],
+ "description": [
+ "group1 Administrators contains only posix.user only"
+ ],
+ "gidnumber": 500,
+ "memberuid": [
+ "posix.user"
+ ],
+ "objectClass": [
+ "top",
+ "posixGroup"
+ ]
+ },
+ "dn": "cn=Administrators,ou=groups,dc=unit,dc=testing",
+ "raw": {
+ "cn": [
+ "Administrators"
+ ],
+ "description": [
+ "group1 Administrators contains only posix.user only"
+ ],
+ "gidnumber": [
+ "500"
+ ],
+ "memberuid": [
+ "posix.user"
+ ],
+ "objectClass": [
+ "top",
+ "posixGroup"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "cn": [
+ "Group3"
+ ],
+ "description": [
+ "group3 Group3 contains only posix.user2 only"
+ ],
+ "gidnumber": 502,
+ "memberuid": [
+ "posix.user2"
+ ],
+ "objectClass": [
+ "top",
+ "posixGroup"
+ ]
+ },
+ "dn": "cn=Group3,ou=groups,dc=unit,dc=testing",
+ "raw": {
+ "cn": [
+ "Group3"
+ ],
+ "description": [
+ "group3 Group3 contains only posix.user2 only"
+ ],
+ "gidnumber": [
+ "502"
+ ],
+ "memberuid": [
+ "posix.user2"
+ ],
+ "objectClass": [
+ "top",
+ "posixGroup"
+ ]
+ }
+ },
+ {
+ "attributes": {
+ "objectClass": [
+ "top",
+ "organizationalUnit"
+ ],
+ "ou": [
+ "Users",
+ "groups"
+ ]
+ },
+ "dn": "ou=groups,dc=unit,dc=testing",
+ "raw": {
+ "objectClass": [
+ "top",
+ "organizationalUnit"
+ ],
+ "ou": [
+ "Users",
+ "groups"
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py b/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py
index 113692b6c4..7b0638876b 100644
--- a/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py
+++ b/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py
@@ -1,8 +1,684 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
-# import frappe
+# License: MIT. See LICENSE
+import frappe
import unittest
+import functools
+import ldap3
+import ssl
+import os
+
+from unittest import mock
+from frappe.integrations.doctype.ldap_settings.ldap_settings import LDAPSettings
+from ldap3 import Server, Connection, MOCK_SYNC, OFFLINE_SLAPD_2_4, OFFLINE_AD_2012_R2
+
+
+class LDAP_TestCase():
+ TEST_LDAP_SERVER = None # must match the 'LDAP Settings' field option
+ TEST_LDAP_SEARCH_STRING = None
+ LDAP_USERNAME_FIELD = None
+ DOCUMENT_GROUP_MAPPINGS = []
+ LDAP_SCHEMA = None
+ LDAP_LDIF_JSON = None
+ TEST_VALUES_LDAP_COMPLEX_SEARCH_STRING = None
+
+ def mock_ldap_connection(f):
+
+ @functools.wraps(f)
+ def wrapped(self, *args, **kwargs):
+
+ with mock.patch('frappe.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.connect_to_ldap') as mock_connection:
+ mock_connection.return_value = self.connection
+
+ self.test_class = LDAPSettings(self.doc)
+
+ # Create a clean doc
+ localdoc = self.doc.copy()
+ frappe.get_doc(localdoc).save()
+
+ rv = f(self, *args, **kwargs)
+
+
+ # Clean-up
+ self.test_class = None
+
+ return rv
+
+ return wrapped
+
+ def clean_test_users():
+ try: # clean up test user 1
+ frappe.get_doc("User", 'posix.user1@unit.testing').delete()
+ except Exception:
+ pass
+
+ try: # clean up test user 2
+ frappe.get_doc("User", 'posix.user2@unit.testing').delete()
+ except Exception:
+ pass
+
+
+ @classmethod
+ def setUpClass(self, ldapServer='OpenLDAP'):
+
+ self.clean_test_users()
+ # Save user data for restoration in tearDownClass()
+ self.user_ldap_settings = frappe.get_doc('LDAP Settings')
+
+ # Create test user1
+ self.user1doc = {
+ 'username': 'posix.user',
+ 'email': 'posix.user1@unit.testing',
+ 'first_name': 'posix'
+ }
+ self.user1doc.update({
+ "doctype": "User",
+ "send_welcome_email": 0,
+ "language": "",
+ "user_type": "System User",
+ })
+
+ user = frappe.get_doc(self.user1doc)
+ user.insert(ignore_permissions=True)
+
+ # Create test user1
+ self.user2doc = {
+ 'username': 'posix.user2',
+ 'email': 'posix.user2@unit.testing',
+ 'first_name': 'posix'
+ }
+ self.user2doc.update({
+ "doctype": "User",
+ "send_welcome_email": 0,
+ "language": "",
+ "user_type": "System User",
+ })
+
+ user = frappe.get_doc(self.user2doc)
+ user.insert(ignore_permissions=True)
+
+
+ # Setup Mock OpenLDAP Directory
+ self.ldap_dc_path = 'dc=unit,dc=testing'
+ self.ldap_user_path = 'ou=users,' + self.ldap_dc_path
+ self.ldap_group_path = 'ou=groups,' + self.ldap_dc_path
+ self.base_dn = 'cn=base_dn_user,' + self.ldap_dc_path
+ self.base_password = 'my_password'
+ self.ldap_server = 'ldap://my_fake_server:389'
+
+
+ self.doc = {
+ "doctype": "LDAP Settings",
+ "enabled": True,
+ "ldap_directory_server": self.TEST_LDAP_SERVER,
+ "ldap_server_url": self.ldap_server,
+ "base_dn": self.base_dn,
+ "password": self.base_password,
+ "ldap_search_path_user": self.ldap_user_path,
+ "ldap_search_string": self.TEST_LDAP_SEARCH_STRING,
+ "ldap_search_path_group": self.ldap_group_path,
+ "ldap_user_creation_and_mapping_section": '',
+ "ldap_email_field": 'mail',
+ "ldap_username_field": self.LDAP_USERNAME_FIELD,
+ "ldap_first_name_field": 'givenname',
+ "ldap_middle_name_field": '',
+ "ldap_last_name_field": 'sn',
+ "ldap_phone_field": 'telephonenumber',
+ "ldap_mobile_field": 'mobile',
+ "ldap_security": '',
+ "ssl_tls_mode": '',
+ "require_trusted_certificate": 'No',
+ "local_private_key_file": '',
+ "local_server_certificate_file": '',
+ "local_ca_certs_file": '',
+ "ldap_group_objectclass": '',
+ "ldap_group_member_attribute": '',
+ "default_role": 'Newsletter Manager',
+ "ldap_groups": self.DOCUMENT_GROUP_MAPPINGS,
+ "ldap_group_field": ''}
+
+ self.server = Server(host=self.ldap_server, port=389, get_info=self.LDAP_SCHEMA)
+
+ self.connection = Connection(
+ self.server,
+ user=self.base_dn,
+ password=self.base_password,
+ read_only=True,
+ client_strategy=MOCK_SYNC)
+
+ self.connection.strategy.entries_from_json(os.path.abspath(os.path.dirname(__file__)) + '/' + self.LDAP_LDIF_JSON)
+
+ self.connection.bind()
+
+
+ @classmethod
+ def tearDownClass(self):
+ try:
+ frappe.get_doc('LDAP Settings').delete()
+
+ except Exception:
+ pass
+
+ try:
+ # return doc back to user data
+ self.user_ldap_settings.save()
+
+ except Exception:
+ pass
+
+ # Clean-up test users
+ self.clean_test_users()
+
+ # Clear OpenLDAP connection
+ self.connection = None
+
+
+ @mock_ldap_connection
+ def test_mandatory_fields(self):
+
+ mandatory_fields = [
+ 'ldap_server_url',
+ 'ldap_directory_server',
+ 'base_dn',
+ 'password',
+ 'ldap_search_path_user',
+ 'ldap_search_path_group',
+ 'ldap_search_string',
+ 'ldap_email_field',
+ 'ldap_username_field',
+ 'ldap_first_name_field',
+ 'require_trusted_certificate',
+ 'default_role'
+ ] # fields that are required to have ldap functioning need to be mandatory
+
+ for mandatory_field in mandatory_fields:
+
+ localdoc = self.doc.copy()
+ localdoc[mandatory_field] = ''
+
+ try:
+
+ frappe.get_doc(localdoc).save()
+
+ self.fail('Document LDAP Settings field [{0}] is not mandatory'.format(mandatory_field))
+
+ except frappe.exceptions.MandatoryError:
+ pass
+
+ except frappe.exceptions.ValidationError:
+ if mandatory_field == 'ldap_search_string':
+ # additional validation is done on this field, pass in this instance
+ pass
+
+
+ for non_mandatory_field in self.doc: # Ensure remaining fields have not been made mandatory
+
+ if non_mandatory_field == 'doctype' or non_mandatory_field in mandatory_fields:
+ continue
+
+ localdoc = self.doc.copy()
+ localdoc[non_mandatory_field] = ''
+
+ try:
+
+ frappe.get_doc(localdoc).save()
+
+ except frappe.exceptions.MandatoryError:
+ self.fail('Document LDAP Settings field [{0}] should not be mandatory'.format(non_mandatory_field))
+
+
+ @mock_ldap_connection
+ def test_validation_ldap_search_string(self):
+
+ invalid_ldap_search_strings = [
+ '',
+ 'uid={0}',
+ '(uid={0}',
+ 'uid={0})',
+ '(&(objectclass=posixgroup)(uid={0})',
+ '&(objectclass=posixgroup)(uid={0}))',
+ '(uid=no_placeholder)'
+ ] # ldap search string must be enclosed in '()' for ldap search to work for finding user and have the same number of opening and closing brackets.
+
+ for invalid_search_string in invalid_ldap_search_strings:
+
+ localdoc = self.doc.copy()
+ localdoc['ldap_search_string'] = invalid_search_string
+
+ try:
+ frappe.get_doc(localdoc).save()
+
+ self.fail("LDAP search string [{0}] should not validate".format(invalid_search_string))
+
+ except frappe.exceptions.ValidationError:
+ pass
+
+
+ def test_connect_to_ldap(self):
+
+ # setup a clean doc with ldap disabled so no validation occurs (this is tested seperatly)
+ local_doc = self.doc.copy()
+ local_doc['enabled'] = False
+ self.test_class = LDAPSettings(self.doc)
+
+ with mock.patch('ldap3.Server') as ldap3_server_method:
+
+ with mock.patch('ldap3.Connection') as ldap3_connection_method:
+ ldap3_connection_method.return_value = self.connection
+
+ with mock.patch('ldap3.Tls') as ldap3_Tls_method:
+
+ function_return = self.test_class.connect_to_ldap(base_dn=self.base_dn, password=self.base_password)
+
+ args, kwargs = ldap3_connection_method.call_args
+
+ prevent_connection_parameters = {
+ # prevent these parameters for security or lack of the und user from being able to configure
+ 'mode': {
+ 'IP_V4_ONLY': 'Locks the user to IPv4 without frappe providing a way to configure',
+ 'IP_V6_ONLY': 'Locks the user to IPv6 without frappe providing a way to configure'
+ },
+ 'auto_bind': {
+ 'NONE': 'ldap3.Connection must autobind with base_dn',
+ 'NO_TLS': 'ldap3.Connection must have TLS',
+ 'TLS_AFTER_BIND': '[Security] ldap3.Connection TLS bind must occur before bind'
+ }
+ }
+
+ for connection_arg in kwargs:
+
+ if connection_arg in prevent_connection_parameters and \
+ kwargs[connection_arg] in prevent_connection_parameters[connection_arg]:
+
+ self.fail('ldap3.Connection was called with {0}, failed reason: [{1}]'.format(
+ kwargs[connection_arg],
+ prevent_connection_parameters[connection_arg][kwargs[connection_arg]]))
+
+ if local_doc['require_trusted_certificate'] == 'Yes':
+ tls_validate = ssl.CERT_REQUIRED
+ tls_version = ssl.PROTOCOL_TLSv1
+ tls_configuration = ldap3.Tls(validate=tls_validate, version=tls_version)
+
+ self.assertTrue(kwargs['auto_bind'] == ldap3.AUTO_BIND_TLS_BEFORE_BIND,
+ 'Security: [ldap3.Connection] autobind TLS before bind with value ldap3.AUTO_BIND_TLS_BEFORE_BIND')
+
+ else:
+ tls_validate = ssl.CERT_NONE
+ tls_version = ssl.PROTOCOL_TLSv1
+ tls_configuration = ldap3.Tls(validate=tls_validate, version=tls_version)
+
+ self.assertTrue(kwargs['auto_bind'],
+ 'ldap3.Connection must autobind')
+
+
+ ldap3_Tls_method.assert_called_with(validate=tls_validate, version=tls_version)
+
+ ldap3_server_method.assert_called_with(host=self.doc['ldap_server_url'], tls=tls_configuration)
+
+ self.assertTrue(kwargs['password'] == self.base_password,
+ 'ldap3.Connection password does not match provided password')
+
+ self.assertTrue(kwargs['raise_exceptions'],
+ 'ldap3.Connection must raise exceptions for error handling')
+
+ self.assertTrue(kwargs['user'] == self.base_dn,
+ 'ldap3.Connection user does not match provided user')
+
+ ldap3_connection_method.assert_called_with(server=ldap3_server_method.return_value,
+ auto_bind=True,
+ password=self.base_password,
+ raise_exceptions=True,
+ read_only=True,
+ user=self.base_dn)
+
+ self.assertTrue(type(function_return) is ldap3.core.connection.Connection,
+ 'The return type must be of ldap3.Connection')
+
+ function_return = self.test_class.connect_to_ldap(base_dn=self.base_dn, password=self.base_password, read_only=False)
+
+ args, kwargs = ldap3_connection_method.call_args
+
+ self.assertFalse(kwargs['read_only'], 'connect_to_ldap() read_only parameter supplied as False but does not match the ldap3.Connection() read_only named parameter')
+
+
+
+
+ @mock_ldap_connection
+ def test_get_ldap_client_settings(self):
+
+ result = self.test_class.get_ldap_client_settings()
+
+ self.assertIsInstance(result, dict)
+
+ self.assertTrue(result['enabled'] == self.doc['enabled']) # settings should match doc
+
+ localdoc = self.doc.copy()
+ localdoc['enabled'] = False
+ frappe.get_doc(localdoc).save()
+
+ result = self.test_class.get_ldap_client_settings()
+
+ self.assertFalse(result['enabled']) # must match the edited doc
+
+
+ @mock_ldap_connection
+ def test_update_user_fields(self):
+
+ test_user_data = {
+ 'username': 'posix.user',
+ 'email': 'posix.user1@unit.testing',
+ 'first_name': 'posix',
+ 'middle_name': 'another',
+ 'last_name': 'user',
+ 'phone': '08 1234 5678',
+ 'mobile_no': '0421 123 456'
+ }
+
+ test_user = frappe.get_doc("User", test_user_data['email'])
+
+ self.test_class.update_user_fields(test_user, test_user_data)
+
+ updated_user = frappe.get_doc("User", test_user_data['email'])
+
+ self.assertTrue(updated_user.middle_name == test_user_data['middle_name'])
+ self.assertTrue(updated_user.last_name == test_user_data['last_name'])
+ self.assertTrue(updated_user.phone == test_user_data['phone'])
+ self.assertTrue(updated_user.mobile_no == test_user_data['mobile_no'])
+
+
+ @mock_ldap_connection
+ def test_sync_roles(self):
+
+ if self.TEST_LDAP_SERVER.lower() == 'openldap':
+ test_user_data = {
+ 'posix.user1': ['Users', 'Administrators', 'default_role', 'frappe_default_all','frappe_default_guest'],
+ 'posix.user2': ['Users', 'Group3', 'default_role', 'frappe_default_all', 'frappe_default_guest']
+ }
+
+ elif self.TEST_LDAP_SERVER.lower() == 'active directory':
+ test_user_data = {
+ 'posix.user1': ['Domain Users', 'Domain Administrators', 'default_role', 'frappe_default_all','frappe_default_guest'],
+ 'posix.user2': ['Domain Users', 'Enterprise Administrators', 'default_role', 'frappe_default_all', 'frappe_default_guest']
+ }
+
+
+ role_to_group_map = {
+ self.doc['ldap_groups'][0]['erpnext_role']: self.doc['ldap_groups'][0]['ldap_group'],
+ self.doc['ldap_groups'][1]['erpnext_role']: self.doc['ldap_groups'][1]['ldap_group'],
+ self.doc['ldap_groups'][2]['erpnext_role']: self.doc['ldap_groups'][2]['ldap_group'],
+ 'Newsletter Manager': 'default_role',
+ 'All': 'frappe_default_all',
+ 'Guest': 'frappe_default_guest',
+
+ }
+
+ # re-create user1 to ensure clean
+ frappe.get_doc("User", 'posix.user1@unit.testing').delete()
+ user = frappe.get_doc(self.user1doc)
+ user.insert(ignore_permissions=True)
+
+ for test_user in test_user_data:
+
+ test_user_doc = frappe.get_doc("User", test_user + '@unit.testing')
+ test_user_roles = frappe.get_roles(test_user + '@unit.testing')
+
+ self.assertTrue(len(test_user_roles) == 2,
+ 'User should only be a part of the All and Guest roles') # check default frappe roles
+
+ self.test_class.sync_roles(test_user_doc, test_user_data[test_user]) # update user roles
+
+ frappe.get_doc("User", test_user + '@unit.testing')
+ updated_user_roles = frappe.get_roles(test_user + '@unit.testing')
+
+ self.assertTrue(len(updated_user_roles) == len(test_user_data[test_user]),
+ 'syncing of the user roles failed. {0} != {1} for user {2}'.format(len(updated_user_roles), len(test_user_data[test_user]), test_user))
+
+ for user_role in updated_user_roles: # match each users role mapped to ldap groups
+
+ self.assertTrue(role_to_group_map[user_role] in test_user_data[test_user],
+ 'during sync_roles(), the user was given role {0} which should not have occured'.format(user_role))
+
+ @mock_ldap_connection
+ def test_create_or_update_user(self):
+
+ test_user_data = {
+ 'posix.user1': ['Users', 'Administrators', 'default_role', 'frappe_default_all','frappe_default_guest'],
+ }
+
+ test_user = 'posix.user1'
+
+ frappe.get_doc("User", test_user + '@unit.testing').delete() # remove user 1
+
+ with self.assertRaises(frappe.exceptions.DoesNotExistError): # ensure user deleted so function can be tested
+ frappe.get_doc("User", test_user + '@unit.testing')
+
+
+ with mock.patch('frappe.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.update_user_fields') \
+ as update_user_fields_method:
+
+ update_user_fields_method.return_value = None
+
+
+ with mock.patch('frappe.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.sync_roles') as sync_roles_method:
+
+ sync_roles_method.return_value = None
+
+ # New user
+ self.test_class.create_or_update_user(self.user1doc, test_user_data[test_user])
+
+ self.assertTrue(sync_roles_method.called, 'User roles need to be updated for a new user')
+ self.assertFalse(update_user_fields_method.called,
+ 'User roles are not required to be updated for a new user, this will occur during logon')
+
+
+ # Existing user
+ self.test_class.create_or_update_user(self.user1doc, test_user_data[test_user])
+
+ self.assertTrue(sync_roles_method.called, 'User roles need to be updated for an existing user')
+ self.assertTrue(update_user_fields_method.called, 'User fields need to be updated for an existing user')
+
+
+ @mock_ldap_connection
+ def test_get_ldap_attributes(self):
+
+ method_return = self.test_class.get_ldap_attributes()
+
+ self.assertTrue(type(method_return) is list)
+
+
+
+ @mock_ldap_connection
+ def test_fetch_ldap_groups(self):
+
+ if self.TEST_LDAP_SERVER.lower() == 'openldap':
+ test_users = {
+ 'posix.user': ['Users', 'Administrators'],
+ 'posix.user2': ['Users', 'Group3']
+
+ }
+ elif self.TEST_LDAP_SERVER.lower() == 'active directory':
+ test_users = {
+ 'posix.user': ['Domain Users', 'Domain Administrators'],
+ 'posix.user2': ['Domain Users', 'Enterprise Administrators']
+
+ }
+
+ for test_user in test_users:
+
+ self.connection.search(
+ search_base=self.ldap_user_path,
+ search_filter=self.TEST_LDAP_SEARCH_STRING.format(test_user),
+ attributes=self.test_class.get_ldap_attributes())
+
+ method_return = self.test_class.fetch_ldap_groups(self.connection.entries[0], self.connection)
+
+ self.assertIsInstance(method_return, list)
+ self.assertTrue(len(method_return) == len(test_users[test_user]))
+
+ for returned_group in method_return:
+
+ self.assertTrue(returned_group in test_users[test_user])
+
+
+
+ @mock_ldap_connection
+ def test_authenticate(self):
+
+ with mock.patch('frappe.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.fetch_ldap_groups') as \
+ fetch_ldap_groups_function:
+
+ fetch_ldap_groups_function.return_value = None
+
+ self.assertTrue(self.test_class.authenticate('posix.user', 'posix_user_password'))
+
+ self.assertTrue(fetch_ldap_groups_function.called,
+ 'As part of authentication function fetch_ldap_groups_function needs to be called')
+
+ invalid_users = [
+ {'prefix_posix.user': 'posix_user_password'},
+ {'posix.user_postfix': 'posix_user_password'},
+ {'posix.user': 'posix_user_password_postfix'},
+ {'posix.user': 'prefix_posix_user_password'},
+ {'posix.user': ''},
+ {'': 'posix_user_password'},
+ {'': ''}
+ ] # All invalid users should return 'invalid username or password'
+
+ for username, password in enumerate(invalid_users):
+
+ with self.assertRaises(frappe.exceptions.ValidationError) as display_massage:
+
+ self.test_class.authenticate(username, password)
+
+ self.assertTrue(str(display_massage.exception).lower() == 'invalid username or password',
+ 'invalid credentials passed authentication [user: {0}, password: {1}]'.format(username, password))
+
+
+ @mock_ldap_connection
+ def test_complex_ldap_search_filter(self):
+
+ ldap_search_filters = self.TEST_VALUES_LDAP_COMPLEX_SEARCH_STRING
+
+ for search_filter in ldap_search_filters:
+
+ self.test_class.ldap_search_string = search_filter
+
+ if 'ACCESS:test3' in search_filter: # posix.user does not have str in ldap.description auth should fail
+
+ with self.assertRaises(frappe.exceptions.ValidationError) as display_massage:
+
+ self.test_class.authenticate('posix.user', 'posix_user_password')
+
+ self.assertTrue(str(display_massage.exception).lower() == 'invalid username or password')
+
+ else:
+ self.assertTrue(self.test_class.authenticate('posix.user', 'posix_user_password'))
+
+
+ def test_reset_password(self):
+
+ self.test_class = LDAPSettings(self.doc)
+
+ # Create a clean doc
+ localdoc = self.doc.copy()
+
+ localdoc['enabled'] = False
+ frappe.get_doc(localdoc).save()
+
+ with mock.patch('frappe.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.connect_to_ldap') as connect_to_ldap:
+ connect_to_ldap.return_value = self.connection
+
+ with self.assertRaises(frappe.exceptions.ValidationError) as validation: # Fail if username string used
+ self.test_class.reset_password('posix.user', 'posix_user_password')
+
+ self.assertTrue(str(validation.exception) == 'No LDAP User found for email: posix.user')
+
+ try:
+ self.test_class.reset_password('posix.user1@unit.testing', 'posix_user_password') # Change Password
+
+ except Exception: # An exception from the tested class is ok, as long as the connection to LDAP was made writeable
+ pass
+
+ connect_to_ldap.assert_called_with(self.base_dn, self.base_password, read_only=False)
+
+
+ @mock_ldap_connection
+ def test_convert_ldap_entry_to_dict(self):
+
+ self.connection.search(
+ search_base=self.ldap_user_path,
+ search_filter=self.TEST_LDAP_SEARCH_STRING.format("posix.user"),
+ attributes=self.test_class.get_ldap_attributes())
+
+ test_ldap_entry = self.connection.entries[0]
+
+ method_return = self.test_class.convert_ldap_entry_to_dict(test_ldap_entry)
+
+ self.assertTrue(type(method_return) is dict) # must be dict
+ self.assertTrue(len(method_return) == 6) # there are 6 fields in mock_ldap for use
+
+
+
+class Test_OpenLDAP(LDAP_TestCase, unittest.TestCase):
+ TEST_LDAP_SERVER = 'OpenLDAP'
+ TEST_LDAP_SEARCH_STRING = '(uid={0})'
+ DOCUMENT_GROUP_MAPPINGS = [
+ {
+ "doctype": "LDAP Group Mapping",
+ "ldap_group": "Administrators",
+ "erpnext_role": "System Manager"
+ },
+ {
+ "doctype": "LDAP Group Mapping",
+ "ldap_group": "Users",
+ "erpnext_role": "Blogger"
+ },
+ {
+ "doctype": "LDAP Group Mapping",
+ "ldap_group": "Group3",
+ "erpnext_role": "Accounts User"
+ }
+ ]
+ LDAP_USERNAME_FIELD = 'uid'
+ LDAP_SCHEMA = OFFLINE_SLAPD_2_4
+ LDAP_LDIF_JSON = 'test_data_ldif_openldap.json'
+
+ TEST_VALUES_LDAP_COMPLEX_SEARCH_STRING = [
+ '(uid={0})',
+ '(&(objectclass=posixaccount)(uid={0}))',
+ '(&(description=*ACCESS:test1*)(uid={0}))', # OpenLDAP has no member of group, use description to filter posix.user has equivilent of AD 'memberOf'
+ '(&(objectclass=posixaccount)(description=*ACCESS:test3*)(uid={0}))' # OpenLDAP has no member of group, use description to filter posix.user doesn't have. equivilent of AD 'memberOf'
+ ]
+
+
+class Test_ActiveDirectory(LDAP_TestCase, unittest.TestCase):
+ TEST_LDAP_SERVER = 'Active Directory'
+ TEST_LDAP_SEARCH_STRING = '(samaccountname={0})'
+ DOCUMENT_GROUP_MAPPINGS = [
+ {
+ "doctype": "LDAP Group Mapping",
+ "ldap_group": "Domain Administrators",
+ "erpnext_role": "System Manager"
+ },
+ {
+ "doctype": "LDAP Group Mapping",
+ "ldap_group": "Domain Users",
+ "erpnext_role": "Blogger"
+ },
+ {
+ "doctype": "LDAP Group Mapping",
+ "ldap_group": "Enterprise Administrators",
+ "erpnext_role": "Accounts User"
+ }
+ ]
+ LDAP_USERNAME_FIELD = 'samaccountname'
+ LDAP_SCHEMA = OFFLINE_AD_2012_R2
+ LDAP_LDIF_JSON = 'test_data_ldif_activedirectory.json'
+
+ TEST_VALUES_LDAP_COMPLEX_SEARCH_STRING = [
+ '(samaccountname={0})',
+ '(&(objectclass=user)(samaccountname={0}))',
+ '(&(description=*ACCESS:test1*)(samaccountname={0}))', # OpenLDAP has no member of group, use description to filter posix.user has equivilent of AD 'memberOf'
+ '(&(objectclass=user)(description=*ACCESS:test3*)(samaccountname={0}))' # OpenLDAP has no member of group, use description to filter posix.user doesn't have. equivilent of AD 'memberOf'
+ ]
-class TestLDAPSettings(unittest.TestCase):
- pass
diff --git a/frappe/integrations/doctype/oauth_authorization_code/oauth_authorization_code.py b/frappe/integrations/doctype/oauth_authorization_code/oauth_authorization_code.py
index 0c7f02844c..5a3f380e84 100644
--- a/frappe/integrations/doctype/oauth_authorization_code/oauth_authorization_code.py
+++ b/frappe/integrations/doctype/oauth_authorization_code/oauth_authorization_code.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/integrations/doctype/oauth_authorization_code/test_oauth_authorization_code.py b/frappe/integrations/doctype/oauth_authorization_code/test_oauth_authorization_code.py
index 6084dd64b4..bc6d29cbdb 100644
--- a/frappe/integrations/doctype/oauth_authorization_code/test_oauth_authorization_code.py
+++ b/frappe/integrations/doctype/oauth_authorization_code/test_oauth_authorization_code.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py b/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py
index 916d0205d2..ff6f96cc4d 100644
--- a/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py
+++ b/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/integrations/doctype/oauth_bearer_token/test_oauth_bearer_token.py b/frappe/integrations/doctype/oauth_bearer_token/test_oauth_bearer_token.py
index 6028cebcf9..965feb4f78 100644
--- a/frappe/integrations/doctype/oauth_bearer_token/test_oauth_bearer_token.py
+++ b/frappe/integrations/doctype/oauth_bearer_token/test_oauth_bearer_token.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/integrations/doctype/oauth_client/oauth_client.py b/frappe/integrations/doctype/oauth_client/oauth_client.py
index 0b449ff968..42fba07ecb 100644
--- a/frappe/integrations/doctype/oauth_client/oauth_client.py
+++ b/frappe/integrations/doctype/oauth_client/oauth_client.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
diff --git a/frappe/integrations/doctype/oauth_client/test_oauth_client.py b/frappe/integrations/doctype/oauth_client/test_oauth_client.py
index a4e50e15d8..fa03fa06e7 100644
--- a/frappe/integrations/doctype/oauth_client/test_oauth_client.py
+++ b/frappe/integrations/doctype/oauth_client/test_oauth_client.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py
index 3ab5df92ac..ec1636659f 100644
--- a/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py
+++ b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/integrations/doctype/oauth_scope/oauth_scope.py b/frappe/integrations/doctype/oauth_scope/oauth_scope.py
index ae579e6b51..cf5fa1f341 100644
--- a/frappe/integrations/doctype/oauth_scope/oauth_scope.py
+++ b/frappe/integrations/doctype/oauth_scope/oauth_scope.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/integrations/doctype/paypal_settings/paypal_settings.py b/frappe/integrations/doctype/paypal_settings/paypal_settings.py
index da045d2c6a..30ac905792 100644
--- a/frappe/integrations/doctype/paypal_settings/paypal_settings.py
+++ b/frappe/integrations/doctype/paypal_settings/paypal_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
"""
# Integrating PayPal
diff --git a/frappe/integrations/doctype/paytm_settings/paytm_settings.py b/frappe/integrations/doctype/paytm_settings/paytm_settings.py
index 9f15d73f09..5255360242 100644
--- a/frappe/integrations/doctype/paytm_settings/paytm_settings.py
+++ b/frappe/integrations/doctype/paytm_settings/paytm_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import json
import requests
diff --git a/frappe/integrations/doctype/paytm_settings/test_paytm_settings.py b/frappe/integrations/doctype/paytm_settings/test_paytm_settings.py
index a00ce86327..425fc87a3f 100644
--- a/frappe/integrations/doctype/paytm_settings/test_paytm_settings.py
+++ b/frappe/integrations/doctype/paytm_settings/test_paytm_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
# import frappe
import unittest
diff --git a/frappe/integrations/doctype/query_parameters/query_parameters.py b/frappe/integrations/doctype/query_parameters/query_parameters.py
index 13fb94dbe3..68e97e9071 100644
--- a/frappe/integrations/doctype/query_parameters/query_parameters.py
+++ b/frappe/integrations/doctype/query_parameters/query_parameters.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py
index d24e15f480..9bbab9db9b 100644
--- a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py
+++ b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
"""
# Integrating RazorPay
@@ -371,6 +371,7 @@ def capture_payment(is_sandbox=False, sanbox_response=None):
doc = frappe.get_doc("Integration Request", doc.name)
doc.status = "Failed"
doc.error = frappe.get_traceback()
+ doc.save()
frappe.log_error(doc.error, '{0} Failed'.format(doc.name))
diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py
index 1346811652..dc824e18b9 100755
--- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py
+++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import os
import os.path
import frappe
diff --git a/frappe/integrations/doctype/s3_backup_settings/test_s3_backup_settings.py b/frappe/integrations/doctype/s3_backup_settings/test_s3_backup_settings.py
index 3aecdf3489..2a586c30d4 100755
--- a/frappe/integrations/doctype/s3_backup_settings/test_s3_backup_settings.py
+++ b/frappe/integrations/doctype/s3_backup_settings/test_s3_backup_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import unittest
class TestS3BackupSettings(unittest.TestCase):
diff --git a/frappe/integrations/doctype/slack_webhook_url/slack_webhook_url.py b/frappe/integrations/doctype/slack_webhook_url/slack_webhook_url.py
index a970fc1f11..a74c0a36ca 100644
--- a/frappe/integrations/doctype/slack_webhook_url/slack_webhook_url.py
+++ b/frappe/integrations/doctype/slack_webhook_url/slack_webhook_url.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/integrations/doctype/slack_webhook_url/test_slack_webhook_url.py b/frappe/integrations/doctype/slack_webhook_url/test_slack_webhook_url.py
index 4285c2c4bc..a256735f81 100644
--- a/frappe/integrations/doctype/slack_webhook_url/test_slack_webhook_url.py
+++ b/frappe/integrations/doctype/slack_webhook_url/test_slack_webhook_url.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import unittest
class TestSlackWebhookURL(unittest.TestCase):
diff --git a/frappe/integrations/doctype/social_login_key/social_login_key.py b/frappe/integrations/doctype/social_login_key/social_login_key.py
index 4a4fcd44f4..195d6800be 100644
--- a/frappe/integrations/doctype/social_login_key/social_login_key.py
+++ b/frappe/integrations/doctype/social_login_key/social_login_key.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe, json
from frappe import _
@@ -80,7 +80,9 @@ class SocialLoginKey(Document):
"redirect_url":"/api/method/frappe.www.login.login_via_github",
"api_endpoint":"user",
"api_endpoint_args":None,
- "auth_url_data":None
+ "auth_url_data": json.dumps({
+ "scope": "user:email"
+ })
}
providers["Google"] = {
diff --git a/frappe/integrations/doctype/social_login_key/test_social_login_key.py b/frappe/integrations/doctype/social_login_key/test_social_login_key.py
index 23effd6a44..73e6a072cb 100644
--- a/frappe/integrations/doctype/social_login_key/test_social_login_key.py
+++ b/frappe/integrations/doctype/social_login_key/test_social_login_key.py
@@ -1,9 +1,15 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.integrations.doctype.social_login_key.social_login_key import BaseUrlNotSetError
import unittest
+from frappe.utils.oauth import login_via_oauth2
+from unittest.mock import patch, MagicMock
+from rauth import OAuth2Service
+from frappe.auth import LoginManager, CookieManager
+from frappe.utils import set_request
+
class TestSocialLoginKey(unittest.TestCase):
def test_adding_frappe_social_login_provider(self):
@@ -14,6 +20,41 @@ class TestSocialLoginKey(unittest.TestCase):
social_login_key.get_social_login_provider(provider_name, initialize=True)
self.assertRaises(BaseUrlNotSetError, social_login_key.insert)
+ def test_github_login_with_private_email(self):
+ github_social_login_setup()
+
+ mock_session = MagicMock()
+ mock_session.get.side_effect = github_response_for_private_email
+
+ with patch.object(OAuth2Service, "get_auth_session", return_value=mock_session):
+ login_via_oauth2("github", "iwriu", {"token": "ewrwerwer"}) # Dummy code and state token
+
+ def test_github_login_with_public_email(self):
+ github_social_login_setup()
+
+ mock_session = MagicMock()
+ mock_session.get.side_effect = github_response_for_public_email
+
+ with patch.object(OAuth2Service, "get_auth_session", return_value=mock_session):
+ login_via_oauth2("github", "iwriu", {"token": "ewrwerwer"}) # Dummy code and state token
+
+ def test_normal_signup_and_github_login(self):
+ github_social_login_setup()
+
+ if not frappe.db.exists("User", "githublogin@example.com"):
+ user = frappe.get_doc({
+ "doctype": "User",
+ "email": "githublogin@example.com",
+ "first_name": "GitHub Login"
+ })
+ user.save(ignore_permissions=True)
+
+ mock_session = MagicMock()
+ mock_session.get.side_effect = github_response_for_login
+
+ with patch.object(OAuth2Service, "get_auth_session", return_value=mock_session):
+ login_via_oauth2("github", "iwriu", {"token": "ewrwerwer"})
+
def make_social_login_key(**kwargs):
kwargs["doctype"] = "Social Login Key"
if not "provider_name" in kwargs:
@@ -34,3 +75,48 @@ def create_or_update_social_login_key():
frappe.db.commit()
return social_login_key
+
+def create_github_social_login_key():
+ if frappe.db.exists("Social Login Key", "github"):
+ return frappe.get_doc("Social Login Key", "github")
+ else:
+ provider_name = "GitHub"
+ social_login_key = make_social_login_key(
+ social_login_provider=provider_name
+ )
+ social_login_key.get_social_login_provider(provider_name, initialize=True)
+
+ # Dummy client_id and client_secret
+ social_login_key.client_id = "h6htd6q"
+ social_login_key.client_secret = "keoererk988ekkhf8w9e8ewrjhhkjer9889"
+ social_login_key.insert(ignore_permissions=True)
+ return social_login_key
+
+def github_response_for_private_email(url, *args, **kwargs):
+ if url == "user":
+ return_value = {"login": "dummy_username", "id": "223342", "email": None, "first_name": "Github Private"}
+ else:
+ return_value = [{"email": "github@example.com", "primary": True, "verified": True}]
+
+ return MagicMock(status_code=200, json=MagicMock(return_value=return_value))
+
+def github_response_for_public_email(url, *args, **kwargs):
+ if url == "user":
+ return_value = {"login": "dummy_username", "id": "223343", "email": "github_public@example.com", "first_name": "Github Public"}
+
+ return MagicMock(status_code=200, json=MagicMock(return_value=return_value))
+
+def github_response_for_login(url, *args, **kwargs):
+ if url == "user":
+ return_value = {"login": "dummy_username", "id": "223346", "email": None, "first_name": "Github Login"}
+ else:
+ return_value = [{"email": "githublogin@example.com", "primary": True, "verified": True}]
+
+ return MagicMock(status_code=200, json=MagicMock(return_value=return_value))
+
+def github_social_login_setup():
+ set_request(path="/random")
+ frappe.local.cookie_manager = CookieManager()
+ frappe.local.login_manager = LoginManager()
+
+ create_github_social_login_key()
diff --git a/frappe/integrations/doctype/stripe_settings/stripe_settings.py b/frappe/integrations/doctype/stripe_settings/stripe_settings.py
index 9bb9c60775..81e40fa72f 100644
--- a/frappe/integrations/doctype/stripe_settings/stripe_settings.py
+++ b/frappe/integrations/doctype/stripe_settings/stripe_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/integrations/doctype/stripe_settings/test_stripe_settings.py b/frappe/integrations/doctype/stripe_settings/test_stripe_settings.py
index ba11c3c38b..e7113d3bd9 100644
--- a/frappe/integrations/doctype/stripe_settings/test_stripe_settings.py
+++ b/frappe/integrations/doctype/stripe_settings/test_stripe_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import unittest
class TestStripeSettings(unittest.TestCase):
diff --git a/frappe/integrations/doctype/token_cache/test_token_cache.py b/frappe/integrations/doctype/token_cache/test_token_cache.py
index 2ffd57403b..5fe648d225 100644
--- a/frappe/integrations/doctype/token_cache/test_token_cache.py
+++ b/frappe/integrations/doctype/token_cache/test_token_cache.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# See license.txt
+# License: MIT. See LICENSE
import unittest
import frappe
diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py
index 3001d12b2b..ea86100cc2 100644
--- a/frappe/integrations/doctype/token_cache/token_cache.py
+++ b/frappe/integrations/doctype/token_cache/token_cache.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
from datetime import datetime, timedelta
diff --git a/frappe/integrations/doctype/webhook/__init__.py b/frappe/integrations/doctype/webhook/__init__.py
index b92497f16c..6dcc0218a3 100644
--- a/frappe/integrations/doctype/webhook/__init__.py
+++ b/frappe/integrations/doctype/webhook/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/integrations/doctype/webhook/test_webhook.py b/frappe/integrations/doctype/webhook/test_webhook.py
index 09ad56a190..a1176aa38b 100644
--- a/frappe/integrations/doctype/webhook/test_webhook.py
+++ b/frappe/integrations/doctype/webhook/test_webhook.py
@@ -1,17 +1,19 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import unittest
import frappe
-from frappe.integrations.doctype.webhook.webhook import get_webhook_headers, get_webhook_data
+from frappe.integrations.doctype.webhook.webhook import get_webhook_headers, get_webhook_data, enqueue_webhook
class TestWebhook(unittest.TestCase):
@classmethod
def setUpClass(cls):
# delete any existing webhooks
- frappe.db.sql("DELETE FROM tabWebhook")
+ frappe.db.delete("Webhook")
+ # Delete existing logs if any
+ frappe.db.delete("Webhook Request Log")
# create test webhooks
cls.create_sample_webhooks()
@@ -44,7 +46,7 @@ class TestWebhook(unittest.TestCase):
@classmethod
def tearDownClass(cls):
# delete any existing webhooks
- frappe.db.sql("DELETE FROM tabWebhook")
+ frappe.db.delete("Webhook")
def setUp(self):
# retrieve or create a User webhook for `after_insert`
@@ -162,3 +164,18 @@ class TestWebhook(unittest.TestCase):
data = get_webhook_data(doc=self.user, webhook=self.webhook)
self.assertEqual(data, {"name": self.user.name})
+
+ def test_webhook_req_log_creation(self):
+ if not frappe.db.get_value('User', 'user2@integration.webhooks.test.com'):
+ user = frappe.get_doc({
+ 'doctype': 'User',
+ 'email': 'user2@integration.webhooks.test.com',
+ 'first_name': 'user2'
+ }).insert()
+ else:
+ user = frappe.get_doc('User', 'user2@integration.webhooks.test.com')
+
+ webhook = frappe.get_doc('Webhook', {'webhook_doctype': 'User'})
+ enqueue_webhook(user, webhook)
+
+ self.assertTrue(frappe.db.get_all('Webhook Request Log', pluck='name'))
\ No newline at end of file
diff --git a/frappe/integrations/doctype/webhook/webhook.json b/frappe/integrations/doctype/webhook/webhook.json
index 85895c052c..880874cb25 100644
--- a/frappe/integrations/doctype/webhook/webhook.json
+++ b/frappe/integrations/doctype/webhook/webhook.json
@@ -18,6 +18,7 @@
"html_condition",
"sb_webhook",
"request_url",
+ "request_method",
"cb_webhook",
"request_structure",
"sb_security",
@@ -154,10 +155,18 @@
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled"
+ },
+ {
+ "default": "POST",
+ "fieldname": "request_method",
+ "fieldtype": "Select",
+ "label": "Request Method",
+ "options": "POST\nPUT\nDELETE",
+ "reqd": 1
}
],
"links": [],
- "modified": "2021-04-14 05:35:28.532049",
+ "modified": "2021-05-25 11:11:28.555291",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Webhook",
diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py
index 1fb2bc6743..8546a9d2f8 100644
--- a/frappe/integrations/doctype/webhook/webhook.py
+++ b/frappe/integrations/doctype/webhook/webhook.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import base64
import datetime
@@ -59,7 +59,6 @@ class Webhook(Document):
if self.request_structure == "Form URL-Encoded":
self.webhook_json = None
elif self.request_structure == "JSON":
- validate_json(self.webhook_json)
validate_template(self.webhook_json)
self.webhook_data = []
@@ -83,18 +82,32 @@ def enqueue_webhook(doc, webhook):
for i in range(3):
try:
- r = requests.post(webhook.request_url, data=json.dumps(data, default=str), headers=headers, timeout=5)
+ r = requests.request(method=webhook.request_method, url=webhook.request_url,
+ data=json.dumps(data, default=str), headers=headers, timeout=5)
r.raise_for_status()
frappe.logger().debug({"webhook_success": r.text})
+ log_request(webhook.request_url, headers, data, r)
break
except Exception as e:
frappe.logger().debug({"webhook_error": e, "try": i + 1})
+ log_request(webhook.request_url, headers, data, r)
sleep(3 * i + 1)
if i != 2:
continue
else:
raise e
+def log_request(url, headers, data, res):
+ request_log = frappe.get_doc({
+ "doctype": "Webhook Request Log",
+ "user": frappe.session.user if frappe.session.user else None,
+ "url": url,
+ "headers": json.dumps(headers, indent=4) if headers else None,
+ "data": json.dumps(data, indent=4) if isinstance(data, dict) else data,
+ "response": json.dumps(res.json(), indent=4) if res else None
+ })
+
+ request_log.save(ignore_permissions=True)
def get_webhook_headers(doc, webhook):
headers = {}
@@ -129,10 +142,3 @@ def get_webhook_data(doc, webhook):
data = json.loads(data)
return data
-
-
-def validate_json(string):
- try:
- json.loads(string)
- except (TypeError, ValueError):
- frappe.throw(_("Request Body consists of an invalid JSON structure"), title=_("Invalid JSON"))
diff --git a/frappe/integrations/doctype/webhook_data/webhook_data.py b/frappe/integrations/doctype/webhook_data/webhook_data.py
index dbd9328482..6037ed5390 100644
--- a/frappe/integrations/doctype/webhook_data/webhook_data.py
+++ b/frappe/integrations/doctype/webhook_data/webhook_data.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/integrations/doctype/webhook_header/webhook_header.py b/frappe/integrations/doctype/webhook_header/webhook_header.py
index 428b287db2..e1944c84bc 100644
--- a/frappe/integrations/doctype/webhook_header/webhook_header.py
+++ b/frappe/integrations/doctype/webhook_header/webhook_header.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/data_import_legacy/__init__.py b/frappe/integrations/doctype/webhook_request_log/__init__.py
similarity index 100%
rename from frappe/core/doctype/data_import_legacy/__init__.py
rename to frappe/integrations/doctype/webhook_request_log/__init__.py
diff --git a/frappe/integrations/doctype/webhook_request_log/test_webhook_request_log.py b/frappe/integrations/doctype/webhook_request_log/test_webhook_request_log.py
new file mode 100644
index 0000000000..5de26a35ed
--- /dev/null
+++ b/frappe/integrations/doctype/webhook_request_log/test_webhook_request_log.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# License: MIT. See LICENSE
+
+# import frappe
+import unittest
+
+class TestWebhookRequestLog(unittest.TestCase):
+ pass
diff --git a/frappe/integrations/doctype/webhook_request_log/webhook_request_log.js b/frappe/integrations/doctype/webhook_request_log/webhook_request_log.js
new file mode 100644
index 0000000000..9ec4f11536
--- /dev/null
+++ b/frappe/integrations/doctype/webhook_request_log/webhook_request_log.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Webhook Request Log', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/integrations/doctype/webhook_request_log/webhook_request_log.json b/frappe/integrations/doctype/webhook_request_log/webhook_request_log.json
new file mode 100644
index 0000000000..96690f6e8c
--- /dev/null
+++ b/frappe/integrations/doctype/webhook_request_log/webhook_request_log.json
@@ -0,0 +1,81 @@
+{
+ "actions": [],
+ "autoname": "WEBHOOK-REQ-.#####",
+ "creation": "2021-05-24 21:35:59.104776",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "user",
+ "headers",
+ "data",
+ "column_break_4",
+ "url",
+ "response"
+ ],
+ "fields": [
+ {
+ "fieldname": "url",
+ "fieldtype": "Data",
+ "label": "URL",
+ "read_only": 1
+ },
+ {
+ "fieldname": "headers",
+ "fieldtype": "Code",
+ "label": "Headers",
+ "options": "JSON",
+ "read_only": 1
+ },
+ {
+ "fieldname": "response",
+ "fieldtype": "Code",
+ "label": "Response",
+ "options": "JSON",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "data",
+ "fieldtype": "Code",
+ "label": "Data",
+ "options": "JSON",
+ "read_only": 1
+ },
+ {
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "label": "User",
+ "options": "User",
+ "read_only": 1
+ }
+ ],
+ "in_create": 1,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-05-26 23:57:58.495261",
+ "modified_by": "Administrator",
+ "module": "Integrations",
+ "name": "Webhook Request Log",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/integrations/doctype/webhook_request_log/webhook_request_log.py b/frappe/integrations/doctype/webhook_request_log/webhook_request_log.py
new file mode 100644
index 0000000000..3f0558ce80
--- /dev/null
+++ b/frappe/integrations/doctype/webhook_request_log/webhook_request_log.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# License: MIT. See LICENSE
+
+# import frappe
+from frappe.model.document import Document
+
+class WebhookRequestLog(Document):
+ pass
diff --git a/frappe/integrations/oauth2_logins.py b/frappe/integrations/oauth2_logins.py
index c38b43beb7..b187d29b34 100644
--- a/frappe/integrations/oauth2_logins.py
+++ b/frappe/integrations/oauth2_logins.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import frappe.utils
diff --git a/frappe/integrations/offsite_backup_utils.py b/frappe/integrations/offsite_backup_utils.py
index 7a263e9d04..416d656d90 100644
--- a/frappe/integrations/offsite_backup_utils.py
+++ b/frappe/integrations/offsite_backup_utils.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
import glob
diff --git a/frappe/integrations/utils.py b/frappe/integrations/utils.py
index 09c20568b5..bda45a765d 100644
--- a/frappe/integrations/utils.py
+++ b/frappe/integrations/utils.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
import json,datetime
@@ -8,35 +8,14 @@ from urllib.parse import parse_qs
from frappe.utils import get_request_session
from frappe import _
-def make_get_request(url, auth=None, headers=None, data=None):
- if not auth:
- auth = ''
- if not data:
- data = {}
- if not headers:
- headers = {}
+def make_request(method, url, auth=None, headers=None, data=None):
+ auth = auth or ''
+ data = data or {}
+ headers = headers or {}
try:
s = get_request_session()
- frappe.flags.integration_request = s.get(url, data={}, auth=auth, headers=headers)
- frappe.flags.integration_request.raise_for_status()
- return frappe.flags.integration_request.json()
-
- except Exception as exc:
- frappe.log_error(frappe.get_traceback())
- raise exc
-
-def make_post_request(url, auth=None, headers=None, data=None):
- if not auth:
- auth = ''
- if not data:
- data = {}
- if not headers:
- headers = {}
-
- try:
- s = get_request_session()
- frappe.flags.integration_request = s.post(url, data=data, auth=auth, headers=headers)
+ frappe.flags.integration_request = s.request(method, url, data=data, auth=auth, headers=headers)
frappe.flags.integration_request.raise_for_status()
if frappe.flags.integration_request.headers.get("content-type") == "text/plain; charset=utf-8":
@@ -47,6 +26,15 @@ def make_post_request(url, auth=None, headers=None, data=None):
frappe.log_error()
raise exc
+def make_get_request(url, **kwargs):
+ return make_request('GET', url, **kwargs)
+
+def make_post_request(url, **kwargs):
+ return make_request('POST', url, **kwargs)
+
+def make_put_request(url, **kwargs):
+ return make_request('PUT', url, **kwargs)
+
def create_request_log(data, integration_type, service_name, name=None, error=None):
if isinstance(data, str):
data = json.loads(data)
diff --git a/frappe/integrations/workspace/integrations/integrations.json b/frappe/integrations/workspace/integrations/integrations.json
index db96304207..b85056e3ef 100644
--- a/frappe/integrations/workspace/integrations/integrations.json
+++ b/frappe/integrations/workspace/integrations/integrations.json
@@ -1,22 +1,20 @@
{
- "category": "Administration",
"charts": [],
+ "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Backup\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Google Services\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Authentication\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Payments\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}]",
"creation": "2020-03-02 15:16:18.714190",
- "developer_mode_only": 0,
- "disable_user_customization": 1,
"docstatus": 0,
"doctype": "Workspace",
- "extends_another_page": 0,
+ "for_user": "",
"hide_custom": 0,
"icon": "integration",
"idx": 0,
- "is_standard": 1,
"label": "Integrations",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Backup",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -25,6 +23,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Dropbox Settings",
+ "link_count": 0,
"link_to": "Dropbox Settings",
"link_type": "DocType",
"onboard": 0,
@@ -35,6 +34,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "S3 Backup Settings",
+ "link_count": 0,
"link_to": "S3 Backup Settings",
"link_type": "DocType",
"onboard": 0,
@@ -45,6 +45,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Google Drive",
+ "link_count": 0,
"link_to": "Google Drive",
"link_type": "DocType",
"onboard": 0,
@@ -54,6 +55,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Google Services",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -62,6 +64,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Google Settings",
+ "link_count": 0,
"link_to": "Google Settings",
"link_type": "DocType",
"onboard": 0,
@@ -72,6 +75,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Google Contacts",
+ "link_count": 0,
"link_to": "Google Contacts",
"link_type": "DocType",
"onboard": 0,
@@ -82,6 +86,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Google Calendar",
+ "link_count": 0,
"link_to": "Google Calendar",
"link_type": "DocType",
"onboard": 0,
@@ -92,6 +97,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Google Drive",
+ "link_count": 0,
"link_to": "Google Drive",
"link_type": "DocType",
"onboard": 0,
@@ -101,6 +107,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Authentication",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -109,6 +116,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Social Login Key",
+ "link_count": 0,
"link_to": "Social Login Key",
"link_type": "DocType",
"onboard": 0,
@@ -119,6 +127,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "LDAP Settings",
+ "link_count": 0,
"link_to": "LDAP Settings",
"link_type": "DocType",
"onboard": 0,
@@ -129,6 +138,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "OAuth Client",
+ "link_count": 0,
"link_to": "OAuth Client",
"link_type": "DocType",
"onboard": 0,
@@ -139,6 +149,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "OAuth Provider Settings",
+ "link_count": 0,
"link_to": "OAuth Provider Settings",
"link_type": "DocType",
"onboard": 0,
@@ -148,6 +159,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Payments",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -156,6 +168,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Braintree Settings",
+ "link_count": 0,
"link_to": "Braintree Settings",
"link_type": "DocType",
"onboard": 0,
@@ -166,6 +179,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "PayPal Settings",
+ "link_count": 0,
"link_to": "PayPal Settings",
"link_type": "DocType",
"onboard": 0,
@@ -176,6 +190,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Razorpay Settings",
+ "link_count": 0,
"link_to": "Razorpay Settings",
"link_type": "DocType",
"onboard": 0,
@@ -186,6 +201,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Stripe Settings",
+ "link_count": 0,
"link_to": "Stripe Settings",
"link_type": "DocType",
"onboard": 0,
@@ -196,6 +212,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Paytm Settings",
+ "link_count": 0,
"link_to": "Paytm Settings",
"link_type": "DocType",
"onboard": 0,
@@ -205,6 +222,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Settings",
+ "link_count": 0,
"onboard": 0,
"type": "Card Break"
},
@@ -213,6 +231,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Webhook",
+ "link_count": 0,
"link_to": "Webhook",
"link_type": "DocType",
"onboard": 0,
@@ -223,38 +242,34 @@
"hidden": 0,
"is_query_report": 0,
"label": "Slack Webhook URL",
+ "link_count": 0,
"link_to": "Slack Webhook URL",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Twilio Settings",
- "link_to": "Twilio Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "SMS Settings",
+ "link_count": 0,
"link_to": "SMS Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
- "modified": "2020-12-01 13:38:39.706680",
+ "modified": "2021-08-05 12:16:00.355268",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Integrations",
"owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
- "shortcuts": []
+ "parent_page": "",
+ "public": 1,
+ "restrict_to_domain": "",
+ "roles": [],
+ "sequence_id": 15,
+ "shortcuts": [],
+ "title": "Integrations"
}
\ No newline at end of file
diff --git a/frappe/middlewares.py b/frappe/middlewares.py
index 05944ec37a..38cb4cea21 100644
--- a/frappe/middlewares.py
+++ b/frappe/middlewares.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import os
diff --git a/frappe/migrate.py b/frappe/migrate.py
index d19e255639..6abc38796f 100644
--- a/frappe/migrate.py
+++ b/frappe/migrate.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import json
import os
@@ -13,11 +13,12 @@ from frappe.utils.connections import check_connection
from frappe.utils.dashboard import sync_dashboards
from frappe.cache_manager import clear_global_cache
from frappe.desk.notifications import clear_notifications
-from frappe.website import render
+from frappe.website.utils import clear_website_cache
from frappe.core.doctype.language.language import sync_languages
from frappe.modules.utils import sync_customizations
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
from frappe.search.website_search import build_index_for_all_routes
+from frappe.database.schema import add_column
def migrate(verbose=True, skip_failing=False, skip_search_index=False):
@@ -26,9 +27,10 @@ def migrate(verbose=True, skip_failing=False, skip_search_index=False):
- run patches
- sync doctypes (schema)
- sync dashboards
+ - sync jobs
- sync fixtures
- - sync desktop icons
- - sync web pages (from /www)
+ - sync customizations
+ - sync languages
- sync web pages (from /www)
- run after migrate hooks
'''
@@ -51,6 +53,7 @@ Otherwise, check the server logs and ensure that all the required services are r
os.remove(touched_tables_file)
try:
+ add_column(doctype="DocType", column_name="migration_hash", fieldtype="Data")
frappe.flags.touched_tables = set()
frappe.flags.in_migrate = True
@@ -65,7 +68,7 @@ Otherwise, check the server logs and ensure that all the required services are r
frappe.modules.patch_handler.run_all(skip_failing)
# sync
- frappe.model.sync.sync_all(verbose=verbose)
+ frappe.model.sync.sync_all()
frappe.translate.clear_cache()
sync_jobs()
sync_fixtures()
@@ -76,7 +79,7 @@ Otherwise, check the server logs and ensure that all the required services are r
frappe.get_doc('Portal Settings', 'Portal Settings').sync_menu()
# syncs statics
- render.clear_cache()
+ clear_website_cache()
# updating installed applications data
frappe.get_single('Installed Applications').update_versions()
diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py
index 75122f5aba..b460db29a7 100644
--- a/frappe/model/__init__.py
+++ b/frappe/model/__init__.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
# model __init__.py
import frappe
@@ -34,12 +34,14 @@ data_fieldtypes = (
'Color',
'Barcode',
'Geolocation',
- 'Duration'
+ 'Duration',
+ 'Icon'
)
no_value_fields = (
'Section Break',
'Column Break',
+ 'Tab Break',
'HTML',
'Table',
'Table MultiSelect',
@@ -52,6 +54,7 @@ no_value_fields = (
display_fieldtypes = (
'Section Break',
'Column Break',
+ 'Tab Break',
'HTML',
'Button',
'Image',
@@ -71,7 +74,8 @@ data_field_options = (
'Email',
'Name',
'Phone',
- 'URL'
+ 'URL',
+ 'Barcode'
)
default_fields = (
@@ -152,32 +156,22 @@ def delete_fields(args_dict, delete=0):
if not fields:
continue
- frappe.db.sql("""
- DELETE FROM `tabDocField`
- WHERE parent='%s' AND fieldname IN (%s)
- """ % (dt, ", ".join(["'{}'".format(f) for f in fields])))
+ frappe.db.delete("DocField", {
+ "parent": dt,
+ "fieldname": ("in", fields),
+ })
# Delete the data/column only if delete is specified
if not delete:
continue
if frappe.db.get_value("DocType", dt, "issingle"):
- frappe.db.sql("""
- DELETE FROM `tabSingles`
- WHERE doctype='%s' AND field IN (%s)
- """ % (dt, ", ".join("'{}'".format(f) for f in fields)))
+ frappe.db.delete("Singles", {
+ "doctype": dt,
+ "field": ("in", fields),
+ })
else:
- existing_fields = frappe.db.multisql({
- "mariadb": "DESC `tab%s`" % dt,
- "postgres": """
- SELECT
- COLUMN_NAME
- FROM
- information_schema.COLUMNS
- WHERE
- TABLE_NAME = 'tab%s';
- """ % dt,
- })
+ existing_fields = frappe.db.describe(dt)
existing_fields = existing_fields and [e[0] for e in existing_fields] or []
fields_need_to_delete = set(fields) & set(existing_fields)
if not fields_need_to_delete:
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index af696e116d..1826cca9a3 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import datetime
from frappe import _
@@ -83,11 +83,15 @@ class BaseDocument(object):
@property
def meta(self):
- if not hasattr(self, "_meta"):
+ if not getattr(self, "_meta", None):
self._meta = frappe.get_meta(self.doctype)
return self._meta
+ def __getstate__(self):
+ self._meta = None
+ return self.__dict__
+
def update(self, d):
""" Update multiple fields of a doctype using a dictionary of key-value pairs.
@@ -263,7 +267,12 @@ class BaseDocument(object):
if isinstance(d[fieldname], list) and df.fieldtype not in table_fields:
frappe.throw(_('Value for {0} cannot be a list').format(_(df.label)))
- if convert_dates_to_str and isinstance(d[fieldname], (datetime.datetime, datetime.time, datetime.timedelta)):
+ if convert_dates_to_str and isinstance(d[fieldname], (
+ datetime.datetime,
+ datetime.date,
+ datetime.time,
+ datetime.timedelta
+ )):
d[fieldname] = str(d[fieldname])
if d[fieldname] == None and ignore_nulls:
@@ -303,7 +312,7 @@ class BaseDocument(object):
doc["doctype"] = self.doctype
for df in self.meta.get_table_fields():
children = self.get(df.fieldname) or []
- doc[df.fieldname] = [d.as_dict(convert_dates_to_str=convert_dates_to_str, no_nulls=no_nulls) for d in children]
+ doc[df.fieldname] = [d.as_dict(convert_dates_to_str=convert_dates_to_str, no_nulls=no_nulls, no_default_fields=no_default_fields) for d in children]
if no_nulls:
for k in list(doc):
@@ -723,6 +732,18 @@ class BaseDocument(object):
if abs(cint(value)) > max_length:
self.throw_length_exceeded_error(df, max_length, value)
+ def _validate_code_fields(self):
+ for field in self.meta.get_code_fields():
+ code_string = self.get(field.fieldname)
+ language = field.get("options")
+
+ if language == "Python":
+ frappe.utils.validate_python_code(code_string, fieldname=field.label, is_expression=False)
+
+ elif language == "PythonExpression":
+ frappe.utils.validate_python_code(code_string, fieldname=field.label)
+
+
def throw_length_exceeded_error(self, df, max_length, value):
if self.parentfield and self.idx:
reference = _("{0}, Row {1}").format(_(self.doctype), self.idx)
@@ -858,7 +879,7 @@ class BaseDocument(object):
return self._precision[cache_key][fieldname]
- def get_formatted(self, fieldname, doc=None, currency=None, absolute_value=False, translated=False):
+ def get_formatted(self, fieldname, doc=None, currency=None, absolute_value=False, translated=False, format=None):
from frappe.utils.formatters import format_value
df = self.meta.get_field(fieldname)
@@ -882,7 +903,7 @@ class BaseDocument(object):
if (absolute_value or doc.get('absolute_value')) and isinstance(val, (int, float)):
val = abs(self.get(fieldname))
- return format_value(val, df=df, doc=doc, currency=currency)
+ return format_value(val, df=df, doc=doc, currency=currency, format=format)
def is_print_hide(self, fieldname, df=None, for_print=True):
"""Returns true if fieldname is to be hidden for print.
@@ -953,7 +974,7 @@ class BaseDocument(object):
return self.cast(val, df)
def cast(self, value, df):
- return cast_fieldtype(df.fieldtype, value)
+ return cast_fieldtype(df.fieldtype, value, show_warning=False)
def _extract_images_from_text_editor(self):
from frappe.core.doctype.file.file import extract_images_from_doc
diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py
index fba6765479..fff2156a10 100644
--- a/frappe/model/create_new.py
+++ b/frappe/model/create_new.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
"""
Create a new document with defaults set
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index 7ed681644f..6181832363 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -1,8 +1,10 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
"""build query for doclistview and return results"""
+from typing import List
import frappe.defaults
+from frappe.query_builder.utils import Column
import frappe.share
from frappe import _
import frappe.permissions
@@ -33,10 +35,10 @@ class DatabaseQuery(object):
join='left join', distinct=False, start=None, page_length=None, limit=None,
ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False,
update=None, add_total_row=None, user_settings=None, reference_doctype=None,
- return_query=False, strict=True, pluck=None, ignore_ddl=False):
+ run=True, strict=True, pluck=None, ignore_ddl=False, parent_doctype=None) -> List:
if not ignore_permissions and \
- not frappe.has_permission(self.doctype, "select", user=user) and \
- not frappe.has_permission(self.doctype, "read", user=user):
+ not frappe.has_permission(self.doctype, "select", user=user, parent_doctype=parent_doctype) and \
+ not frappe.has_permission(self.doctype, "read", user=user, parent_doctype=parent_doctype):
frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(self.doctype))
raise frappe.PermissionError(self.doctype)
@@ -85,7 +87,7 @@ class DatabaseQuery(object):
self.user = user or frappe.session.user
self.update = update
self.user_settings_fields = copy.deepcopy(self.fields)
- self.return_query = return_query
+ self.run = run
self.strict = strict
self.ignore_ddl = ignore_ddl
@@ -102,8 +104,6 @@ class DatabaseQuery(object):
if not self.columns: return []
result = self.build_and_run()
- if return_query:
- return result
if with_comment_count and not as_list and self.doctype:
self.add_comment_count(result)
@@ -135,11 +135,8 @@ class DatabaseQuery(object):
%(order_by)s
%(limit)s""" % args
- if self.return_query:
- return query
- else:
- return frappe.db.sql(query, as_dict=not self.as_list, debug=self.debug,
- update=self.update, ignore_ddl=self.ignore_ddl)
+ return frappe.db.sql(query, as_dict=not self.as_list, debug=self.debug,
+ update=self.update, ignore_ddl=self.ignore_ddl, run=self.run)
def prepare_args(self):
self.parse_args()
@@ -321,7 +318,8 @@ class DatabaseQuery(object):
doctype = table_name[4:-1]
ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read'
- if not self.flags.ignore_permissions and not frappe.has_permission(doctype, ptype=ptype):
+ if not self.flags.ignore_permissions and \
+ not frappe.has_permission(doctype, ptype=ptype, parent_doctype=self.doctype):
frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(doctype))
raise frappe.PermissionError(doctype)
@@ -492,7 +490,7 @@ class DatabaseQuery(object):
if f.operator in ('>', '<') and (f.fieldname in ('creation', 'modified')):
value = cstr(f.value)
- fallback = "NULL"
+ fallback = "'0001-01-01 00:00:00'"
elif f.operator.lower() in ('between') and \
(f.fieldname in ('creation', 'modified') or (df and (df.fieldtype=="Date" or df.fieldtype=="Datetime"))):
@@ -546,8 +544,13 @@ class DatabaseQuery(object):
value = flt(f.value)
fallback = 0
+ if isinstance(f.value, Column):
+ can_be_null = False # added to avoid the ifnull/coalesce addition
+ quote = '"' if frappe.conf.db_type == 'postgres' else "`"
+ value = f"{tname}.{quote}{f.value.name}{quote}"
+
# escape value
- if isinstance(value, str) and not f.operator.lower() == 'between':
+ elif isinstance(value, str) and not f.operator.lower() == 'between':
value = f"{frappe.db.escape(value, percent=False)}"
if (
@@ -591,8 +594,8 @@ class DatabaseQuery(object):
self.conditions.append(self.get_share_condition())
else:
- #if has if_owner permission skip user perm check
- if role_permissions.get("has_if_owner_enabled") and role_permissions.get("if_owner", {}):
+ # skip user perm check if owner constraint is required
+ if requires_owner_constraint(role_permissions):
self.match_conditions.append(
f"`tab{self.doctype}`.`owner` = {frappe.db.escape(self.user, percent=False)}"
)
@@ -889,3 +892,22 @@ def get_date_range(operator, value):
timespan = period_map[operator] + ' ' + timespan_map[value] if operator != 'timespan' else value
return get_timespan_date_range(timespan)
+
+def requires_owner_constraint(role_permissions):
+ """Returns True if "select" or "read" isn't available without being creator."""
+
+ if not role_permissions.get("has_if_owner_enabled"):
+ return
+
+ if_owner_perms = role_permissions.get("if_owner")
+ if not if_owner_perms:
+ return
+
+ # has select or read without if owner, no need for constraint
+ for perm_type in ("select", "read"):
+ if role_permissions.get(perm_type) and perm_type not in if_owner_perms:
+ return
+
+ # not checking if either select or read if present in if_owner_perms
+ # because either of those is required to perform a query
+ return True
diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py
index cc88cfa106..ac976e976c 100644
--- a/frappe/model/delete_doc.py
+++ b/frappe/model/delete_doc.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import os
import shutil
@@ -10,7 +10,7 @@ import frappe.model.meta
from frappe import _
from frappe import get_module_path
from frappe.model.dynamic_links import get_dynamic_link_map
-from frappe.core.doctype.file.file import remove_all
+from frappe.utils.file_manager import remove_all
from frappe.utils.password import delete_all_passwords_for
from frappe.model.naming import revert_series_if_last
from frappe.utils.global_search import delete_for_document
@@ -65,12 +65,12 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa
update_flags(doc, flags, ignore_permissions)
check_permission_and_not_submitted(doc)
- frappe.db.sql("delete from `tabCustom Field` where dt = %s", name)
- frappe.db.sql("delete from `tabClient Script` where dt = %s", name)
- frappe.db.sql("delete from `tabProperty Setter` where doc_type = %s", name)
- frappe.db.sql("delete from `tabReport` where ref_doctype=%s", name)
- frappe.db.sql("delete from `tabCustom DocPerm` where parent=%s", name)
- frappe.db.sql("delete from `__global_search` where doctype=%s", name)
+ frappe.db.delete("Custom Field", {"dt": name})
+ frappe.db.delete("Client Script", {"dt": name})
+ frappe.db.delete("Property Setter", {"doc_type": name})
+ frappe.db.delete("Report", {"ref_doctype": name})
+ frappe.db.delete("Custom DocPerm", {"parent": name})
+ frappe.db.delete("__global_search", {"doctype": name})
delete_from_table(doctype, name, ignore_doctypes, None)
@@ -162,10 +162,9 @@ def update_naming_series(doc):
def delete_from_table(doctype, name, ignore_doctypes, doc):
if doctype!="DocType" and doctype==name:
- frappe.db.sql("delete from `tabSingles` where `doctype`=%s", name)
+ frappe.db.delete("Singles", {"doctype": name})
else:
- frappe.db.sql("delete from `tab{0}` where `name`=%s".format(doctype), name)
-
+ frappe.db.delete(doctype, {"name": name})
# get child tables
if doc:
tables = [d.options for d in doc.meta.get_table_fields()]
@@ -191,7 +190,7 @@ def delete_from_table(doctype, name, ignore_doctypes, doc):
# delete from child tables
for t in list(set(tables)):
if t not in ignore_doctypes:
- frappe.db.sql("delete from `tab%s` where parenttype=%s and parent = %s" % (t, '%s', '%s'), (doctype, name))
+ frappe.db.delete(t, {"parenttype": doctype, "parent": name})
def update_flags(doc, flags=None, ignore_permissions=False):
if ignore_permissions:
@@ -324,9 +323,10 @@ def delete_dynamic_links(doctype, name):
def delete_references(doctype, reference_doctype, reference_name,
reference_doctype_field = 'reference_doctype', reference_name_field = 'reference_name'):
- frappe.db.sql('''delete from `tab{0}`
- where {1}=%s and {2}=%s'''.format(doctype, reference_doctype_field, reference_name_field), # nosec
- (reference_doctype, reference_name))
+ frappe.db.delete(doctype, {
+ reference_doctype_field: reference_doctype,
+ reference_name_field: reference_name
+ })
def clear_references(doctype, reference_doctype, reference_name,
reference_doctype_field = 'reference_doctype', reference_name_field = 'reference_name'):
@@ -339,8 +339,10 @@ def clear_references(doctype, reference_doctype, reference_name,
(reference_doctype, reference_name))
def clear_timeline_references(link_doctype, link_name):
- frappe.db.sql("""DELETE FROM `tabCommunication Link`
- WHERE `tabCommunication Link`.link_doctype=%s AND `tabCommunication Link`.link_name=%s""", (link_doctype, link_name))
+ frappe.db.delete("Communication Link", {
+ "link_doctype": link_doctype,
+ "link_name": link_name
+ })
def insert_feed(doc):
if (
diff --git a/frappe/model/docfield.py b/frappe/model/docfield.py
index 6360c3866d..c173561b1e 100644
--- a/frappe/model/docfield.py
+++ b/frappe/model/docfield.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
"""docfield utililtes"""
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 61160e1f01..411d447d0f 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -1,11 +1,11 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import time
from frappe import _, msgprint, is_whitelisted
from frappe.utils import flt, cstr, now, get_datetime_str, file_lock, date_diff
from frappe.model.base_document import BaseDocument, get_controller
-from frappe.model.naming import set_new_name
+from frappe.model.naming import set_new_name, gen_new_name_for_cancelled_doc
from werkzeug.exceptions import NotFound, Forbidden
import hashlib, json
from frappe.model import optional_fields, table_fields
@@ -385,15 +385,15 @@ class Document(BaseDocument):
[self.name, self.doctype, fieldname] + rows)
if len(deleted_rows) > 0:
# delete rows that do not match the ones in the document
- frappe.db.sql("""delete from `tab{0}` where name in ({1})""".format(df.options,
- ','.join(['%s'] * len(deleted_rows))), tuple(row[0] for row in deleted_rows))
+ frappe.db.delete(df.options, {"name": ("in", tuple(row[0] for row in deleted_rows))})
else:
# no rows found, delete all rows
- frappe.db.sql("""delete from `tab{0}` where parent=%s
- and parenttype=%s and parentfield=%s""".format(df.options),
- (self.name, self.doctype, fieldname))
-
+ frappe.db.delete(df.options, {
+ "parent": self.name,
+ "parenttype": self.doctype,
+ "parentfield": fieldname
+ })
def get_doc_before_save(self):
return getattr(self, '_doc_before_save', None)
@@ -451,7 +451,9 @@ class Document(BaseDocument):
def update_single(self, d):
"""Updates values for Single type Document in `tabSingles`."""
- frappe.db.sql("""delete from `tabSingles` where doctype=%s""", self.doctype)
+ frappe.db.delete("Singles", {
+ "doctype": self.doctype
+ })
for field, value in d.items():
if field != "doctype":
frappe.db.sql("""insert into `tabSingles` (doctype, field, value)
@@ -492,6 +494,7 @@ class Document(BaseDocument):
self._validate_selects()
self._validate_non_negative()
self._validate_length()
+ self._validate_code_fields()
self._extract_images_from_text_editor()
self._sanitize_content()
self._save_passwords()
@@ -503,6 +506,7 @@ class Document(BaseDocument):
d._validate_selects()
d._validate_non_negative()
d._validate_length()
+ d._validate_code_fields()
d._extract_images_from_text_editor()
d._sanitize_content()
d._save_passwords()
@@ -705,7 +709,6 @@ class Document(BaseDocument):
else:
tmp = frappe.db.sql("""select modified, docstatus from `tab{0}`
where name = %s for update""".format(self.doctype), self.name, as_dict=True)
-
if not tmp:
frappe.throw(_("Record does not exist"))
else:
@@ -916,8 +919,12 @@ class Document(BaseDocument):
@whitelist.__func__
def _cancel(self):
- """Cancel the document. Sets `docstatus` = 2, then saves."""
+ """Cancel the document. Sets `docstatus` = 2, then saves.
+ """
self.docstatus = 2
+ new_name = gen_new_name_for_cancelled_doc(self)
+ frappe.rename_doc(self.doctype, self.name, new_name, force=True, show_alert=False)
+ self.name = new_name
self.save()
@whitelist.__func__
@@ -1060,7 +1067,10 @@ class Document(BaseDocument):
self.set("modified", now())
self.set("modified_by", frappe.session.user)
- self.load_doc_before_save()
+ # load but do not reload doc_before_save because before_change or on_change might expect it
+ if not self.get_doc_before_save():
+ self.load_doc_before_save()
+
# to trigger notification on value change
self.run_method('before_change')
diff --git a/frappe/model/dynamic_links.py b/frappe/model/dynamic_links.py
index 676c86d7da..7311b39b30 100644
--- a/frappe/model/dynamic_links.py
+++ b/frappe/model/dynamic_links.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/model/mapper.py b/frappe/model/mapper.py
index fa8858d950..bde4fb6d73 100644
--- a/frappe/model/mapper.py
+++ b/frappe/model/mapper.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import json
import frappe
diff --git a/frappe/model/meta.py b/frappe/model/meta.py
index b212324208..cd0d8e0f3a 100644
--- a/frappe/model/meta.py
+++ b/frappe/model/meta.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
# metadata
@@ -15,8 +15,9 @@ Example:
'''
from datetime import datetime
+import click
import frappe, json, os
-from frappe.utils import cstr, cint, cast_fieldtype
+from frappe.utils import cstr, cint, cast
from frappe.model import default_fields, no_value_fields, optional_fields, data_fieldtypes, table_fields
from frappe.model.document import Document
from frappe.model.base_document import BaseDocument
@@ -141,6 +142,9 @@ class Meta(Document):
def get_image_fields(self):
return self.get("fields", {"fieldtype": "Attach Image"})
+ def get_code_fields(self):
+ return self.get("fields", {"fieldtype": "Code"})
+
def get_set_only_once_fields(self):
'''Return fields with `set_only_once` set'''
if not hasattr(self, "_set_only_once_fields"):
@@ -319,24 +323,24 @@ class Meta(Document):
for ps in property_setters:
if ps.doctype_or_field=='DocType':
- self.set(ps.property, cast_fieldtype(ps.property_type, ps.value))
+ self.set(ps.property, cast(ps.property_type, ps.value))
elif ps.doctype_or_field=='DocField':
for d in self.fields:
if d.fieldname == ps.field_name:
- d.set(ps.property, cast_fieldtype(ps.property_type, ps.value))
+ d.set(ps.property, cast(ps.property_type, ps.value))
break
elif ps.doctype_or_field=='DocType Link':
for d in self.links:
if d.name == ps.row_name:
- d.set(ps.property, cast_fieldtype(ps.property_type, ps.value))
+ d.set(ps.property, cast(ps.property_type, ps.value))
break
elif ps.doctype_or_field=='DocType Action':
for d in self.actions:
if d.name == ps.row_name:
- d.set(ps.property, cast_fieldtype(ps.property_type, ps.value))
+ d.set(ps.property, cast(ps.property_type, ps.value))
break
def add_custom_links_and_actions(self):
@@ -504,6 +508,9 @@ class Meta(Document):
if not data.non_standard_fieldnames:
data.non_standard_fieldnames = {}
+ if not data.internal_links:
+ data.internal_links = {}
+
for link in dashboard_links:
link.added = False
if link.hidden:
@@ -511,24 +518,32 @@ class Meta(Document):
for group in data.transactions:
group = frappe._dict(group)
+
+ # For internal links parent doctype will be the key
+ doctype = link.parent_doctype or link.link_doctype
# group found
if link.group and group.label == link.group:
- if link.link_doctype not in group.get('items'):
- group.get('items').append(link.link_doctype)
+ if doctype not in group.get('items'):
+ group.get('items').append(doctype)
link.added = True
if not link.added:
# group not found, make a new group
data.transactions.append(dict(
label = link.group,
- items = [link.link_doctype]
+ items = [link.parent_doctype or link.link_doctype]
))
- if link.link_fieldname != data.fieldname:
- if data.fieldname:
- data.non_standard_fieldnames[link.link_doctype] = link.link_fieldname
- else:
+ if not link.is_child_table:
+ if link.link_fieldname != data.fieldname:
+ if data.fieldname:
+ data.non_standard_fieldnames[link.link_doctype] = link.link_fieldname
+ else:
+ data.fieldname = link.link_fieldname
+ elif link.is_child_table:
+ if not data.fieldname:
data.fieldname = link.link_fieldname
+ data.internal_links[link.parent_doctype] = [link.table_fieldname, link.link_fieldname]
def get_row_template(self):
@@ -644,27 +659,48 @@ def get_default_df(fieldname):
fieldtype = "Data"
)
-def trim_tables(doctype=None):
+def trim_tables(doctype=None, dry_run=False, quiet=False):
"""
Removes database fields that don't exist in the doctype (json or custom field). This may be needed
as maintenance since removing a field in a DocType doesn't automatically
delete the db field.
"""
- ignore_fields = default_fields + optional_fields
-
- filters={ "issingle": 0 }
+ UPDATED_TABLES = {}
+ filters = {"issingle": 0}
if doctype:
filters["name"] = doctype
- for doctype in frappe.db.get_all("DocType", filters=filters):
- doctype = doctype.name
- columns = frappe.db.get_table_columns(doctype)
- fields = frappe.get_meta(doctype).get_fieldnames_with_value()
- columns_to_remove = [f for f in list(set(columns) - set(fields)) if f not in ignore_fields
- and not f.startswith("_")]
- if columns_to_remove:
- print(doctype, "columns removed:", columns_to_remove)
- columns_to_remove = ", ".join("drop `{0}`".format(c) for c in columns_to_remove)
- query = """alter table `tab{doctype}` {columns}""".format(
- doctype=doctype, columns=columns_to_remove)
- frappe.db.sql_ddl(query)
+ for doctype in frappe.db.get_all("DocType", filters=filters, pluck="name"):
+ try:
+ dropped_columns = trim_table(doctype, dry_run=dry_run)
+ if dropped_columns:
+ UPDATED_TABLES[doctype] = dropped_columns
+ except frappe.db.TableMissingError:
+ if quiet:
+ continue
+ click.secho(f"Ignoring missing table for DocType: {doctype}", fg="yellow", err=True)
+ click.secho(f"Consider removing record in the DocType table for {doctype}", fg="yellow", err=True)
+ except Exception as e:
+ if quiet:
+ continue
+ click.echo(e, err=True)
+
+ return UPDATED_TABLES
+
+
+def trim_table(doctype, dry_run=True):
+ frappe.cache().hdel('table_columns', f"tab{doctype}")
+ ignore_fields = default_fields + optional_fields
+ columns = frappe.db.get_table_columns(doctype)
+ fields = frappe.get_meta(doctype, cached=False).get_fieldnames_with_value()
+ is_internal = lambda f: f not in ignore_fields and not f.startswith("_")
+ columns_to_remove = [
+ f for f in list(set(columns) - set(fields)) if is_internal(f)
+ ]
+ DROPPED_COLUMNS = columns_to_remove[:]
+
+ if columns_to_remove and not dry_run:
+ columns_to_remove = ", ".join(f"DROP `{c}`" for c in columns_to_remove)
+ frappe.db.sql_ddl(f"ALTER TABLE `tab{doctype}` {columns_to_remove}")
+
+ return DROPPED_COLUMNS
diff --git a/frappe/model/naming.py b/frappe/model/naming.py
index fe136adce8..deea6698b3 100644
--- a/frappe/model/naming.py
+++ b/frappe/model/naming.py
@@ -1,11 +1,23 @@
+"""utilities to generate a document name based on various rules defined.
+
+NOTE:
+Till version 13, whenever a submittable document is amended it's name is set to orig_name-X,
+where X is a counter and it increments when amended again and so on.
+
+From Version 14, The naming pattern is changed in a way that amended documents will
+have the original name `orig_name` instead of `orig_name-X`. To make this happen
+the cancelled document naming pattern is changed to 'orig_name-CANC-X'.
+"""
+
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
from frappe.utils import now_datetime, cint, cstr
import re
from frappe.model import log_types
+from frappe.query_builder import DocType
def set_new_name(doc):
@@ -28,7 +40,7 @@ def set_new_name(doc):
doc.name = None
if getattr(doc, "amended_from", None):
- _set_amended_name(doc)
+ doc.name = _get_amended_name(doc)
return
elif getattr(doc.meta, "issingle", False):
@@ -183,7 +195,15 @@ def parse_naming_series(parts, doctype='', doc=''):
def getseries(key, digits):
# series created ?
- current = frappe.db.sql("SELECT `current` FROM `tabSeries` WHERE `name`=%s FOR UPDATE", (key,))
+ # Using frappe.qb as frappe.get_values does not allow order_by=None
+ series = DocType("Series")
+ current = (
+ frappe.qb.from_(series)
+ .where(series.name == key)
+ .for_update()
+ .select("current")
+ ).run()
+
if current and current[0][0] is not None:
current = current[0][0]
# yes, update it
@@ -221,6 +241,18 @@ def revert_series_if_last(key, name, doc=None):
* prefix = #### and hashes = 2021 (hash doesn't exist)
* will search hash in key then accordingly get prefix = ""
"""
+ if hasattr(doc, 'amended_from'):
+ # Do not revert the series if the document is amended.
+ if doc.amended_from:
+ return
+
+ # Get document name by parsing incase of fist cancelled document
+ if doc.docstatus == 2 and not doc.amended_from:
+ if doc.name.endswith('-CANC'):
+ name, _ = NameParser.parse_docname(doc.name, sep='-CANC')
+ else:
+ name, _ = NameParser.parse_docname(doc.name, sep='-CANC-')
+
if ".#" in key:
prefix, hashes = key.rsplit(".", 1)
if "#" not in hashes:
@@ -237,7 +269,13 @@ def revert_series_if_last(key, name, doc=None):
prefix = parse_naming_series(prefix.split('.'), doc=doc)
count = cint(name.replace(prefix, ""))
- current = frappe.db.sql("SELECT `current` FROM `tabSeries` WHERE `name`=%s FOR UPDATE", (prefix,))
+ series = DocType("Series")
+ current = (
+ frappe.qb.from_(series)
+ .where(series.name == prefix)
+ .for_update()
+ .select("current")
+ ).run()
if current and current[0][0]==count:
frappe.db.sql("UPDATE `tabSeries` SET `current` = `current` - 1 WHERE `name`=%s", prefix)
@@ -303,16 +341,9 @@ def append_number_if_name_exists(doctype, value, fieldname="name", separator="-"
return value
-def _set_amended_name(doc):
- am_id = 1
- am_prefix = doc.amended_from
- if frappe.db.get_value(doc.doctype, doc.amended_from, "amended_from"):
- am_id = cint(doc.amended_from.split("-")[-1]) + 1
- am_prefix = "-".join(doc.amended_from.split("-")[:-1]) # except the last hyphen
-
- doc.name = am_prefix + "-" + str(am_id)
- return doc.name
-
+def _get_amended_name(doc):
+ name, _ = NameParser(doc).parse_amended_from()
+ return name
def _field_autoname(autoname, doc, skip_slicing=None):
"""
@@ -323,7 +354,6 @@ def _field_autoname(autoname, doc, skip_slicing=None):
name = (cstr(doc.get(fieldname)) or "").strip()
return name
-
def _prompt_autoname(autoname, doc):
"""
Generate a name using Prompt option. This simply means the user will have to set the name manually.
@@ -331,7 +361,7 @@ def _prompt_autoname(autoname, doc):
"""
# set from __newname in save.py
if not doc.name:
- frappe.throw(_("Name not set via prompt"))
+ frappe.throw(_("Please set the document name"))
def _format_autoname(autoname, doc):
"""
@@ -354,3 +384,83 @@ def _format_autoname(autoname, doc):
name = re.sub(r"(\{[\w | #]+\})", get_param_value_for_match, autoname_value)
return name
+
+class NameParser:
+ """Parse document name and return parts of it.
+
+ NOTE: It handles cancellend and amended doc parsing for now. It can be expanded.
+ """
+ def __init__(self, doc):
+ self.doc = doc
+
+ def parse_amended_from(self):
+ """
+ Cancelled document naming will be in one of these formats
+
+ * original_name-X-CANC - This is introduced to migrate old style naming to new style
+ * original_name-CANC - This is introduced to migrate old style naming to new style
+ * original_name-CANC-X - This is the new style naming
+
+ New style naming: In new style naming amended documents will have original name. That says,
+ when a document gets cancelled we need rename the document by adding `-CANC-X` to the end
+ so that amended documents can use the original name.
+
+ Old style naming: cancelled documents stay with original name and when amended, amended one
+ gets a new name as `original_name-X`. To bring new style naming we had to change the existing
+ cancelled document names and that is done by adding `-CANC` to cancelled documents through patch.
+ """
+ if not getattr(self.doc, 'amended_from', None):
+ return (None, None)
+
+ # Handle old style cancelled documents (original_name-X-CANC, original_name-CANC)
+ if self.doc.amended_from.endswith('-CANC'):
+ name, _ = self.parse_docname(self.doc.amended_from, '-CANC')
+ amended_from_doc = frappe.get_all(
+ self.doc.doctype,
+ filters = {'name': self.doc.amended_from},
+ fields = ['amended_from'],
+ limit=1)
+
+ # Handle format original_name-X-CANC.
+ if amended_from_doc and amended_from_doc[0].amended_from:
+ return self.parse_docname(name, '-')
+ return name, None
+
+ # Handle new style cancelled documents
+ return self.parse_docname(self.doc.amended_from, '-CANC-')
+
+ @classmethod
+ def parse_docname(cls, name, sep='-'):
+ split_list = name.rsplit(sep, 1)
+
+ if len(split_list) == 1:
+ return (name, None)
+ return (split_list[0], split_list[1])
+
+def get_cancelled_doc_latest_counter(tname, docname):
+ """Get the latest counter used for cancelled docs of given docname.
+ """
+ name_prefix = f'{docname}-CANC-'
+
+ rows = frappe.db.sql("""
+ select
+ name
+ from `tab{tname}`
+ where
+ name like %(name_prefix)s and docstatus=2
+ """.format(tname=tname), {'name_prefix': name_prefix+'%'}, as_dict=1)
+
+ if not rows:
+ return -1
+ return max([int(row.name.replace(name_prefix, '') or -1) for row in rows])
+
+def gen_new_name_for_cancelled_doc(doc):
+ """Generate a new name for cancelled document.
+ """
+ if getattr(doc, "amended_from", None):
+ name, _ = NameParser(doc).parse_amended_from()
+ else:
+ name = doc.name
+
+ counter = get_cancelled_doc_latest_counter(doc.doctype, name)
+ return f'{name}-CANC-{counter+1}'
diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py
index 9b8ac2574d..ee9044b73e 100644
--- a/frappe/model/rename_doc.py
+++ b/frappe/model/rename_doc.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _, bold
from frappe.model.dynamic_links import get_dynamic_link_map
@@ -7,6 +7,7 @@ from frappe.model.naming import validate_name
from frappe.model.utils.user_settings import sync_user_settings, update_user_settings_data
from frappe.utils import cint
from frappe.utils.password import rename_password
+from frappe.query_builder import Field
@frappe.whitelist()
@@ -191,8 +192,14 @@ def update_autoname_field(doctype, new, meta):
def validate_rename(doctype, new, meta, merge, force, ignore_permissions):
# using for update so that it gets locked and someone else cannot edit it while this rename is going on!
- exists = frappe.db.sql("select name from `tab{doctype}` where name=%s for update".format(doctype=doctype), new)
- exists = exists[0][0] if exists else None
+ exists = (
+ frappe.qb.from_(doctype)
+ .where(Field("name") == new)
+ .for_update()
+ .select("name")
+ .run(pluck=True)
+ )
+ exists = exists[0] if exists else None
if merge and not exists:
frappe.msgprint(_("{0} {1} does not exist, select a new target to merge").format(doctype, new), raise_exception=1)
@@ -458,7 +465,7 @@ def bulk_rename(doctype, rows=None, via_console = False):
"""Bulk rename documents
:param doctype: DocType to be renamed
- :param rows: list of documents as `((oldname, newname), ..)`"""
+ :param rows: list of documents as `((oldname, newname, merge(optional)), ..)`"""
if not rows:
frappe.throw(_("Please select a valid csv file with data"))
@@ -471,8 +478,9 @@ def bulk_rename(doctype, rows=None, via_console = False):
for row in rows:
# if row has some content
if len(row) > 1 and row[0] and row[1]:
+ merge = len(row) > 2 and (row[2] == "1" or row[2].lower() == "true")
try:
- if rename_doc(doctype, row[0], row[1]):
+ if rename_doc(doctype, row[0], row[1], merge=merge):
msg = _("Successful: {0} to {1}").format(row[0], row[1])
frappe.db.commit()
else:
diff --git a/frappe/model/sync.py b/frappe/model/sync.py
index 28f9deb25d..42bb16cbc2 100644
--- a/frappe/model/sync.py
+++ b/frappe/model/sync.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
"""
Sync's doctype and docfields from txt files to database
perms will get synced only if none exist
@@ -10,62 +10,67 @@ from frappe.modules.import_file import import_file_by_path
from frappe.modules.patch_handler import block_user
from frappe.utils import update_progress_bar
-def sync_all(force=0, verbose=False, reset_permissions=False):
+
+def sync_all(force=0, reset_permissions=False):
block_user(True)
for app in frappe.get_installed_apps():
- sync_for(app, force, verbose=verbose, reset_permissions=reset_permissions)
+ sync_for(app, force, reset_permissions=reset_permissions)
block_user(False)
frappe.clear_cache()
-def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_permissions=False):
+
+def sync_for(app_name, force=0, reset_permissions=False):
files = []
if app_name == "frappe":
# these need to go first at time of install
- for d in (("core", "docfield"),
- ("core", "docperm"),
- ("core", "doctype_action"),
- ("core", "doctype_link"),
- ("core", "role"),
- ("core", "has_role"),
- ("core", "doctype"),
- ("core", "user"),
- ("custom", "custom_field"),
- ("custom", "property_setter"),
- ("website", "web_form"),
- ("website", "web_template"),
- ("website", "web_form_field"),
- ("website", "portal_menu_item"),
- ("data_migration", "data_migration_mapping_detail"),
- ("data_migration", "data_migration_mapping"),
- ("data_migration", "data_migration_plan_mapping"),
- ("data_migration", "data_migration_plan"),
- ("desk", "number_card"),
- ("desk", "dashboard_chart"),
- ("desk", "dashboard"),
- ("desk", "onboarding_permission"),
- ("desk", "onboarding_step"),
- ("desk", "onboarding_step_map"),
- ("desk", "module_onboarding"),
- ("desk", "workspace_link"),
- ("desk", "workspace_chart"),
- ("desk", "workspace_shortcut"),
- ("desk", "workspace")):
- files.append(os.path.join(frappe.get_app_path("frappe"), d[0],
- "doctype", d[1], d[1] + ".json"))
+
+ FRAPPE_PATH = frappe.get_app_path("frappe")
+
+ for core_module in ["docfield", "docperm", "doctype_action", "doctype_link", "role", "has_role", "doctype"]:
+ files.append(os.path.join(FRAPPE_PATH, "core", "doctype", core_module, f"{core_module}.json"))
+
+ for custom_module in ["custom_field", "property_setter"]:
+ files.append(os.path.join(FRAPPE_PATH, "custom", "doctype", custom_module, f"{custom_module}.json"))
+
+ for website_module in ["web_form", "web_template", "web_form_field", "portal_menu_item"]:
+ files.append(os.path.join(FRAPPE_PATH, "website", "doctype", website_module, f"{website_module}.json"))
+
+ for data_migration_module in [
+ "data_migration_mapping_detail",
+ "data_migration_mapping",
+ "data_migration_plan_mapping",
+ "data_migration_plan",
+ ]:
+ files.append(os.path.join(FRAPPE_PATH, "data_migration", "doctype", data_migration_module, f"{data_migration_module}.json"))
+
+ for desk_module in [
+ "number_card",
+ "dashboard_chart",
+ "dashboard",
+ "onboarding_permission",
+ "onboarding_step",
+ "onboarding_step_map",
+ "module_onboarding",
+ "workspace_link",
+ "workspace_chart",
+ "workspace_shortcut",
+ "workspace",
+ ]:
+ files.append(os.path.join(FRAPPE_PATH, "desk", "doctype", desk_module, f"{desk_module}.json"))
for module_name in frappe.local.app_modules.get(app_name) or []:
folder = os.path.dirname(frappe.get_module(app_name + "." + module_name).__file__)
- get_doc_files(files, folder)
+ files = get_doc_files(files=files, start_path=folder)
l = len(files)
+
if l:
for i, doc_path in enumerate(files):
- import_file_by_path(doc_path, force=force, ignore_version=True,
- reset_permissions=reset_permissions, for_sync=True)
+ import_file_by_path(doc_path, force=force, ignore_version=True, reset_permissions=reset_permissions)
frappe.db.commit()
@@ -75,15 +80,36 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe
# print each progress bar on new line
print()
+
def get_doc_files(files, start_path):
"""walk and sync all doctypes and pages"""
- # load in sequence - warning for devs
- document_types = ['doctype', 'page', 'report', 'dashboard_chart_source', 'print_format',
- 'website_theme', 'web_form', 'web_template', 'notification', 'print_style',
- 'data_migration_mapping', 'data_migration_plan', 'workspace',
- 'onboarding_step', 'module_onboarding']
+ files = files or []
+ # load in sequence - warning for devs
+ document_types = [
+ "doctype",
+ "page",
+ "report",
+ "dashboard_chart_source",
+ "print_format",
+ "web_page",
+ "website_theme",
+ "web_form",
+ "web_template",
+ "notification",
+ "print_style",
+ "data_migration_mapping",
+ "data_migration_plan",
+ "workspace",
+ "onboarding_step",
+ "module_onboarding",
+ "form_tour",
+ "client_script",
+ "server_script",
+ "custom_field",
+ "property_setter",
+ ]
for doctype in document_types:
doctype_path = os.path.join(start_path, doctype)
if os.path.exists(doctype_path):
@@ -93,3 +119,5 @@ def get_doc_files(files, start_path):
if os.path.exists(doc_path):
if not doc_path in files:
files.append(doc_path)
+
+ return files
diff --git a/frappe/model/utils/__init__.py b/frappe/model/utils/__init__.py
index 47615182e4..4cdca5e394 100644
--- a/frappe/model/utils/__init__.py
+++ b/frappe/model/utils/__init__.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
from frappe.utils import cstr
diff --git a/frappe/model/utils/link_count.py b/frappe/model/utils/link_count.py
index 7562aaae45..404b6ec855 100644
--- a/frappe/model/utils/link_count.py
+++ b/frappe/model/utils/link_count.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/model/utils/rename_field.py b/frappe/model/utils/rename_field.py
index 9fe9d64041..c9c454b7e8 100644
--- a/frappe/model/utils/rename_field.py
+++ b/frappe/model/utils/rename_field.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import json
from frappe.model import no_value_fields, table_fields
diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py
index fa2f557370..e74d88c0f2 100644
--- a/frappe/model/workflow.py
+++ b/frappe/model/workflow.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.utils import cint
diff --git a/frappe/modules.txt b/frappe/modules.txt
index ae10c3ad55..a707ca853e 100644
--- a/frappe/modules.txt
+++ b/frappe/modules.txt
@@ -9,7 +9,6 @@ Integrations
Printing
Contacts
Data Migration
-Chat
Social
Automation
Event Streaming
\ No newline at end of file
diff --git a/frappe/modules/export_file.py b/frappe/modules/export_file.py
index ae9f11d53b..ab6ffd4985 100644
--- a/frappe/modules/export_file.py
+++ b/frappe/modules/export_file.py
@@ -1,12 +1,12 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe, os
import frappe.model
from frappe.modules import scrub, get_module_path, scrub_dt_dn
def export_doc(doc):
- export_to_files([[doc.doctype, doc.name]])
+ write_document_file(doc)
def export_to_files(record_list=None, record_module=None, verbose=0, create_init=None):
"""
@@ -21,16 +21,10 @@ def export_to_files(record_list=None, record_module=None, verbose=0, create_init
write_document_file(frappe.get_doc(record[0], record[1]), record_module, create_init=create_init, folder_name=folder_name)
def write_document_file(doc, record_module=None, create_init=True, folder_name=None):
- newdoc = doc.as_dict(no_nulls=True)
- doc.run_method("before_export", newdoc)
-
- # strip out default fields from children
- for df in doc.meta.get_table_fields():
- for d in newdoc.get(df.fieldname):
- for fieldname in frappe.model.default_fields:
- if fieldname in d:
- del d[fieldname]
+ doc_export = doc.as_dict(no_nulls=True)
+ doc.run_method("before_export", doc_export)
+ doc_export = strip_default_fields(doc, doc_export)
module = record_module or get_module_name(doc)
# create folder
@@ -39,10 +33,36 @@ def write_document_file(doc, record_module=None, create_init=True, folder_name=N
else:
folder = create_folder(module, doc.doctype, doc.name, create_init)
- # write the data file
fname = scrub(doc.name)
+ write_code_files(folder, fname, doc, doc_export)
+
+ # write the data file
with open(os.path.join(folder, fname + ".json"), 'w+') as txtfile:
- txtfile.write(frappe.as_json(newdoc))
+ txtfile.write(frappe.as_json(doc_export))
+
+def strip_default_fields(doc, doc_export):
+ # strip out default fields from children
+ if doc.doctype == "DocType" and doc.migration_hash:
+ del doc_export["migration_hash"]
+
+ for df in doc.meta.get_table_fields():
+ for d in doc_export.get(df.fieldname):
+ for fieldname in frappe.model.default_fields:
+ if fieldname in d:
+ del d[fieldname]
+
+ return doc_export
+
+def write_code_files(folder, fname, doc, doc_export):
+ '''Export code files and strip from values'''
+ if hasattr(doc, 'get_code_fields'):
+ for key, extn in doc.get_code_fields().items():
+ if doc.get(key):
+ with open(os.path.join(folder, fname + "." + extn), 'w+') as txtfile:
+ txtfile.write(doc.get(key))
+
+ # remove from exporting
+ del doc_export[key]
def get_module_name(doc):
if doc.doctype == 'Module Def':
@@ -57,7 +77,10 @@ def get_module_name(doc):
return module
def create_folder(module, dt, dn, create_init):
- module_path = get_module_path(module)
+ if frappe.db.get_value('Module Def', module, 'custom'):
+ module_path = get_custom_module_path(module)
+ else:
+ module_path = get_module_path(module)
dt, dn = scrub_dt_dn(dt, dn)
@@ -72,6 +95,23 @@ def create_folder(module, dt, dn, create_init):
return folder
+def get_custom_module_path(module):
+ package = frappe.db.get_value('Module Def', module, 'package')
+ if not package:
+ frappe.throw('Package must be set for custom Module {module}'.format(module=module))
+
+ path = os.path.join(get_package_path(package), scrub(module))
+ if not os.path.exists(path):
+ os.makedirs(path)
+
+ return path
+
+def get_package_path(package):
+ path = os.path.join(frappe.get_site_path('packages'), frappe.db.get_value('Package', package, 'package_name'))
+ if not os.path.exists(path):
+ os.makedirs(path)
+ return path
+
def create_init_py(module_path, dt, dn):
def create_if_not_exists(path):
initpy = os.path.join(path, '__init__.py')
diff --git a/frappe/modules/import_file.py b/frappe/modules/import_file.py
index e743f0c3da..cf8ec46d76 100644
--- a/frappe/modules/import_file.py
+++ b/frappe/modules/import_file.py
@@ -1,31 +1,53 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-import frappe, os, json
-from frappe.modules import get_module_path, scrub_dt_dn
-from frappe.utils import get_datetime_str
+# License: MIT. See LICENSE
+import hashlib
+import json
+import os
+
+import frappe
from frappe.model.base_document import get_controller
+from frappe.modules import get_module_path, scrub_dt_dn
+from frappe.query_builder import DocType
+from frappe.utils import get_datetime_str, now
+
+
+def caclulate_hash(path: str) -> str:
+ """Calculate md5 hash of the file in binary mode
+
+ Args:
+ path (str): Path to the file to be hashed
+
+ Returns:
+ str: The calculated hash
+ """
+ hash_md5 = hashlib.md5()
+ with open(path, "rb") as f:
+ for chunk in iter(lambda: f.read(4096), b""):
+ hash_md5.update(chunk)
+ return hash_md5.hexdigest()
+
ignore_values = {
"Report": ["disabled", "prepared_report", "add_total_row"],
"Print Format": ["disabled"],
"Notification": ["enabled"],
"Print Style": ["disabled"],
- "Module Onboarding": ['is_complete'],
- "Onboarding Step": ['is_complete', 'is_skipped']
+ "Module Onboarding": ["is_complete"],
+ "Onboarding Step": ["is_complete", "is_skipped"],
}
ignore_doctypes = [""]
+
def import_files(module, dt=None, dn=None, force=False, pre_process=None, reset_permissions=False):
if type(module) is list:
out = []
for m in module:
- out.append(import_file(m[0], m[1], m[2], force=force, pre_process=pre_process,
- reset_permissions=reset_permissions))
+ out.append(import_file(m[0], m[1], m[2], force=force, pre_process=pre_process, reset_permissions=reset_permissions))
return out
else:
- return import_file(module, dt, dn, force=force, pre_process=pre_process,
- reset_permissions=reset_permissions)
+ return import_file(module, dt, dn, force=force, pre_process=pre_process, reset_permissions=reset_permissions)
+
def import_file(module, dt, dn, force=False, pre_process=None, reset_permissions=False):
"""Sync a file from txt if modifed, return false if not updated"""
@@ -33,85 +55,166 @@ def import_file(module, dt, dn, force=False, pre_process=None, reset_permissions
ret = import_file_by_path(path, force, pre_process=pre_process, reset_permissions=reset_permissions)
return ret
+
def get_file_path(module, dt, dn):
dt, dn = scrub_dt_dn(dt, dn)
- path = os.path.join(get_module_path(module),
- os.path.join(dt, dn, dn + ".json"))
+ path = os.path.join(get_module_path(module), os.path.join(dt, dn, f"{dn}.json"))
return path
-def import_file_by_path(path, force=False, data_import=False, pre_process=None, ignore_version=None,
- reset_permissions=False, for_sync=False):
+
+def import_file_by_path(path: str,force: bool = False,data_import: bool = False,pre_process = None,ignore_version: bool = None,reset_permissions: bool = False):
+ """Import file from the given path
+
+ Some conditions decide if a file should be imported or not.
+ Evaluation takes place in the order they are mentioned below.
+
+ - Check if `force` is true. Import the file. If not, move ahead.
+ - Get `db_modified_timestamp`(value of the modified field in the database for the file).
+ If the return is `none,` this file doesn't exist in the DB, so Import the file. If not, move ahead.
+ - Check if there is a hash in DB for that file. If there is, Calculate the Hash of the file to import and compare it with the one in DB if they are not equal.
+ Import the file. If Hash doesn't exist, move ahead.
+ - Check if `db_modified_timestamp` is older than the timestamp in the file; if it is, we import the file.
+
+ If timestamp comparison happens for doctypes, that means the Hash for it doesn't exist.
+ So, even if the timestamp is newer on DB (When comparing timestamps), we import the file and add the calculated Hash to the DB.
+ So in the subsequent imports, we can use hashes to compare. As a precautionary measure, the timestamp is updated to the current time as well.
+
+ Args:
+ path (str): Path to the file.
+ force (bool, optional): Load the file without checking any conditions. Defaults to False.
+ data_import (bool, optional): [description]. Defaults to False.
+ pre_process ([type], optional): Any preprocesing that may need to take place on the doc. Defaults to None.
+ ignore_version (bool, optional): ignore current version. Defaults to None.
+ reset_permissions (bool, optional): reset permissions for the file. Defaults to False.
+
+ Returns:
+ [bool]: True if import takes place. False if it wasn't imported.
+ """
+ frappe.flags.dt = frappe.flags.dt or []
try:
docs = read_doc_from_file(path)
except IOError:
- print (path + " missing")
+ print(f"{path} missing")
return
+ calculated_hash = caclulate_hash(path)
+
if docs:
if not isinstance(docs, list):
docs = [docs]
for doc in docs:
- if not force:
- # check if timestamps match
- db_modified = frappe.db.get_value(doc['doctype'], doc['name'], 'modified')
- if db_modified and doc.get('modified')==get_datetime_str(db_modified):
+
+ # modified timestamp in db, none if doctype's first import
+ db_modified_timestamp = frappe.db.get_value(doc["doctype"], doc["name"], "modified")
+ is_db_timestamp_latest = db_modified_timestamp and doc.get("modified") <= get_datetime_str(db_modified_timestamp)
+
+ if not force or db_modified_timestamp:
+ try:
+ stored_hash = frappe.db.get_value(doc["doctype"], doc["name"], "migration_hash")
+ except Exception:
+ frappe.flags.dt += [doc["doctype"]]
+ stored_hash = None
+
+ # if hash exists and is equal no need to update
+ if stored_hash and stored_hash == calculated_hash:
return False
- original_modified = doc.get("modified")
+ # if hash doesn't exist, check if db timestamp is same as json timestamp, add hash if from doctype
+ if is_db_timestamp_latest and doc["doctype"] != "DocType":
+ return False
- frappe.flags.in_import = True
- import_doc(doc, force=force, data_import=data_import, pre_process=pre_process,
- ignore_version=ignore_version, reset_permissions=reset_permissions)
- frappe.flags.in_import = False
+ import_doc(
+ docdict=doc,
+ force=force,
+ data_import=data_import,
+ pre_process=pre_process,
+ ignore_version=ignore_version,
+ reset_permissions=reset_permissions,
+ path=path,
+ )
- if original_modified:
- # since there is a new timestamp on the file, update timestamp in
- if doc["doctype"] == doc["name"] and doc["name"]!="DocType":
- frappe.db.sql("""update tabSingles set value=%s where field="modified" and doctype=%s""",
- (original_modified, doc["name"]))
- else:
- frappe.db.sql("update `tab%s` set modified=%s where name=%s" % \
- (doc['doctype'], '%s', '%s'),
- (original_modified, doc['name']))
+ if doc["doctype"] == "DocType":
+ doctype_table = DocType("DocType")
+ frappe.qb.update(
+ doctype_table
+ ).set(
+ doctype_table.migration_hash, calculated_hash
+ ).where(
+ doctype_table.name == doc["name"]
+ ).run()
+
+ new_modified_timestamp = doc.get("modified")
+
+ # if db timestamp is newer, hash must have changed, must update db timestamp
+ if is_db_timestamp_latest and doc["doctype"] == "DocType":
+ new_modified_timestamp = now()
+
+ if new_modified_timestamp:
+ update_modified(new_modified_timestamp, doc)
return True
+
+def is_timestamp_changed(doc):
+ # check if timestamps match
+ db_modified = frappe.db.get_value(doc["doctype"], doc["name"], "modified")
+ return not (db_modified and doc.get("modified") == get_datetime_str(db_modified))
+
+
def read_doc_from_file(path):
doc = None
if os.path.exists(path):
- with open(path, 'r') as f:
+ with open(path, "r") as f:
try:
doc = json.loads(f.read())
except ValueError:
print("bad json: {0}".format(path))
raise
else:
- raise IOError('%s missing' % path)
+ raise IOError("%s missing" % path)
return doc
-def import_doc(docdict, force=False, data_import=False, pre_process=None,
- ignore_version=None, reset_permissions=False):
+
+def update_modified(original_modified, doc):
+ # since there is a new timestamp on the file, update timestamp in
+ if doc["doctype"] == doc["name"] and doc["name"] != "DocType":
+ singles_table = DocType("Singles")
+
+ frappe.qb.update(
+ singles_table
+ ).set(
+ singles_table.value,original_modified
+ ).where(
+ singles_table.field == "modified"
+ ).where(
+ singles_table.doctype == doc["name"]
+ ).run()
+ else:
+ doctype_table = DocType(doc['doctype'])
+
+ frappe.qb.update(doctype_table
+ ).set(
+ doctype_table.modified, original_modified
+ ).where(
+ doctype_table.name == doc["name"]
+ ).run()
+
+def import_doc(docdict, force=False, data_import=False, pre_process=None, ignore_version=None, reset_permissions=False, path=None):
frappe.flags.in_import = True
docdict["__islocal"] = 1
- controller = get_controller(docdict['doctype'])
- if controller and hasattr(controller, 'prepare_for_import') and callable(getattr(controller, 'prepare_for_import')):
+ controller = get_controller(docdict["doctype"])
+ if controller and hasattr(controller, "prepare_for_import") and callable(getattr(controller, "prepare_for_import")):
controller.prepare_for_import(docdict)
doc = frappe.get_doc(docdict)
- # Note on Tree DocTypes:
- # The tree structure is maintained in the database via the fields "lft" and
- # "rgt". They are automatically set and kept up-to-date. Importing them
- # would destroy any existing tree structure.
- if getattr(doc.meta, 'is_tree', None) and any([doc.lft, doc.rgt]):
- print('Ignoring values of `lft` and `rgt` for {} "{}"'.format(doc.doctype, doc.name))
- doc.lft = None
- doc.rgt = None
+ reset_tree_properties(doc)
+ load_code_properties(doc, path)
doc.run_method("before_import")
@@ -119,27 +222,9 @@ def import_doc(docdict, force=False, data_import=False, pre_process=None,
if pre_process:
pre_process(doc)
- ignore = []
-
if frappe.db.exists(doc.doctype, doc.name):
+ delete_old_doc(doc, reset_permissions)
- old_doc = frappe.get_doc(doc.doctype, doc.name)
-
- if doc.doctype in ignore_values:
- # update ignore values
- for key in ignore_values.get(doc.doctype) or []:
- doc.set(key, old_doc.get(key))
-
- # update ignored docs into new doc
- for df in doc.meta.get_table_fields():
- if df.options in ignore_doctypes and not reset_permissions:
- doc.set(df.fieldname, [])
- ignore.append(df.options)
-
- # delete old
- frappe.delete_doc(doc.doctype, doc.name, force=1, ignore_doctypes=ignore, for_reload=True)
-
- doc.flags.ignore_children_type = ignore
doc.flags.ignore_links = True
if not data_import:
doc.flags.ignore_validate = True
@@ -149,3 +234,49 @@ def import_doc(docdict, force=False, data_import=False, pre_process=None,
doc.insert()
frappe.flags.in_import = False
+
+ return doc
+
+
+def load_code_properties(doc, path):
+ """Load code files stored in separate files with extensions"""
+ if path:
+ if hasattr(doc, "get_code_fields"):
+ dirname, filename = os.path.split(path)
+ for key, extn in doc.get_code_fields().items():
+ codefile = os.path.join(dirname, filename.split(".")[0] + "." + extn)
+ if os.path.exists(codefile):
+ with open(codefile, "r") as txtfile:
+ doc.set(key, txtfile.read())
+
+
+def delete_old_doc(doc, reset_permissions):
+ ignore = []
+ old_doc = frappe.get_doc(doc.doctype, doc.name)
+
+ if doc.doctype in ignore_values:
+ # update ignore values
+ for key in ignore_values.get(doc.doctype) or []:
+ doc.set(key, old_doc.get(key))
+
+ # update ignored docs into new doc
+ for df in doc.meta.get_table_fields():
+ if df.options in ignore_doctypes and not reset_permissions:
+ doc.set(df.fieldname, [])
+ ignore.append(df.options)
+
+ # delete old
+ frappe.delete_doc(doc.doctype, doc.name, force=1, ignore_doctypes=ignore, for_reload=True)
+
+ doc.flags.ignore_children_type = ignore
+
+
+def reset_tree_properties(doc):
+ # Note on Tree DocTypes:
+ # The tree structure is maintained in the database via the fields "lft" and
+ # "rgt". They are automatically set and kept up-to-date. Importing them
+ # would destroy any existing tree structure.
+ if getattr(doc.meta, "is_tree", None) and any([doc.lft, doc.rgt]):
+ print('Ignoring values of `lft` and `rgt` for {} "{}"'.format(doc.doctype, doc.name))
+ doc.lft = None
+ doc.rgt = None
diff --git a/frappe/modules/patch_handler.py b/frappe/modules/patch_handler.py
index 029234d5d9..8dfb27c0b8 100644
--- a/frappe/modules/patch_handler.py
+++ b/frappe/modules/patch_handler.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
"""
Execute Patch Files
diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py
index 0f3e57a5a0..bbfd63a277 100644
--- a/frappe/modules/utils.py
+++ b/frappe/modules/utils.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
"""
Utilities for using modules
"""
@@ -114,8 +114,7 @@ def sync_customizations_for_doctype(data, folder):
doc.db_insert()
if custom_doctype != 'Custom Field':
- frappe.db.sql('delete from `tab{0}` where `{1}` =%s'.format(
- custom_doctype, doctype_fieldname), doc_type)
+ frappe.db.delete(custom_doctype, {doctype_fieldname: doc_type})
for d in data[key]:
_insert(d)
diff --git a/frappe/monitor.py b/frappe/monitor.py
index 34ca7d67f7..6bad03dfe9 100644
--- a/frappe/monitor.py
+++ b/frappe/monitor.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
from datetime import datetime
import json
diff --git a/frappe/parallel_test_runner.py b/frappe/parallel_test_runner.py
index 2f83b88572..1a6892d30d 100644
--- a/frappe/parallel_test_runner.py
+++ b/frappe/parallel_test_runner.py
@@ -15,10 +15,9 @@ if click_ctx:
click_ctx.color = True
class ParallelTestRunner():
- def __init__(self, app, site, build_number=1, total_builds=1, with_coverage=False):
+ def __init__(self, app, site, build_number=1, total_builds=1):
self.app = app
self.site = site
- self.with_coverage = with_coverage
self.build_number = frappe.utils.cint(build_number) or 1
self.total_builds = frappe.utils.cint(total_builds)
self.setup_test_site()
@@ -53,12 +52,9 @@ class ParallelTestRunner():
def run_tests(self):
self.test_result = ParallelTestResult(stream=sys.stderr, descriptions=True, verbosity=2)
- self.start_coverage()
-
for test_file_info in self.get_test_file_list():
self.run_tests_for_file(test_file_info)
- self.save_coverage()
self.print_result()
def run_tests_for_file(self, file_info):
@@ -107,45 +103,6 @@ class ParallelTestRunner():
if os.environ.get('CI'):
sys.exit(1)
- def start_coverage(self):
- if self.with_coverage:
- from coverage import Coverage
- from frappe.utils import get_bench_path
-
- # Generate coverage report only for app that is being tested
- source_path = os.path.join(get_bench_path(), 'apps', self.app)
- incl = [
- '*.py',
- ]
- omit = [
- '*.js',
- '*.xml',
- '*.pyc',
- '*.css',
- '*.less',
- '*.scss',
- '*.vue',
- '*.pyc',
- '*.html',
- '*/test_*',
- '*/node_modules/*',
- '*/doctype/*/*_dashboard.py',
- '*/patches/*',
- ]
-
- if self.app == 'frappe':
- omit.append('*/tests/*')
- omit.append('*/commands/*')
-
- self.coverage = Coverage(source=[source_path], omit=omit, include=incl)
- self.coverage.start()
-
- def save_coverage(self):
- if not self.with_coverage:
- return
- self.coverage.stop()
- self.coverage.save()
-
def get_test_file_list(self):
test_list = get_all_tests(self.app)
split_size = frappe.utils.ceil(len(test_list) / self.total_builds)
@@ -241,7 +198,7 @@ class ParallelTestWithOrchestrator(ParallelTestRunner):
- get-next-test-spec (, )
- test-completed (, )
'''
- def __init__(self, app, site, with_coverage=False):
+ def __init__(self, app, site):
self.orchestrator_url = os.environ.get('ORCHESTRATOR_URL')
if not self.orchestrator_url:
click.echo('ORCHESTRATOR_URL environment variable not found!')
@@ -254,7 +211,7 @@ class ParallelTestWithOrchestrator(ParallelTestRunner):
click.echo('CI_BUILD_ID environment variable not found!')
sys.exit(1)
- ParallelTestRunner.__init__(self, app, site, with_coverage=with_coverage)
+ ParallelTestRunner.__init__(self, app, site)
def run_tests(self):
self.test_status = 'ongoing'
@@ -281,7 +238,9 @@ class ParallelTestWithOrchestrator(ParallelTestRunner):
self.call_orchestrator('test-completed')
return super().print_result()
- def call_orchestrator(self, endpoint, data={}):
+ def call_orchestrator(self, endpoint, data=None):
+ if data is None:
+ data = {}
# add repo token header
# build id in header
headers = {
diff --git a/frappe/patches.txt b/frappe/patches.txt
index 7605d8ea2b..8309b2df57 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -178,5 +178,10 @@ frappe.patches.v13_0.rename_list_view_setting_to_list_view_settings
frappe.patches.v13_0.remove_twilio_settings
frappe.patches.v12_0.rename_uploaded_files_with_proper_name
frappe.patches.v13_0.queryreport_columns
+execute:frappe.reload_doc('core', 'doctype', 'doctype')
frappe.patches.v13_0.jinja_hook
frappe.patches.v13_0.update_notification_channel_if_empty
+frappe.patches.v14_0.drop_data_import_legacy
+frappe.patches.v14_0.rename_cancelled_documents
+frappe.patches.v14_0.update_workspace2 # 20.09.2021
+frappe.patches.v14_0.update_github_endpoints #08-11-2021
diff --git a/frappe/patches/v10_0/modify_smallest_currency_fraction.py b/frappe/patches/v10_0/modify_smallest_currency_fraction.py
index c9ae477359..9469d546ce 100644
--- a/frappe/patches/v10_0/modify_smallest_currency_fraction.py
+++ b/frappe/patches/v10_0/modify_smallest_currency_fraction.py
@@ -1,5 +1,5 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/patches/v10_0/set_default_locking_time.py b/frappe/patches/v10_0/set_default_locking_time.py
index 045fa0e3fa..11993e1163 100644
--- a/frappe/patches/v10_0/set_default_locking_time.py
+++ b/frappe/patches/v10_0/set_default_locking_time.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/patches/v11_0/apply_customization_to_custom_doctype.py b/frappe/patches/v11_0/apply_customization_to_custom_doctype.py
index 49b68ed240..7e84c5ae24 100644
--- a/frappe/patches/v11_0/apply_customization_to_custom_doctype.py
+++ b/frappe/patches/v11_0/apply_customization_to_custom_doctype.py
@@ -28,7 +28,7 @@ def execute():
for prop in property_setters:
property_setter_map[prop.field_name] = prop
- frappe.db.sql('DELETE FROM `tabProperty Setter` WHERE `name`=%s', prop.name)
+ frappe.db.delete("Property Setter", {"name": prop.name})
meta = frappe.get_meta(doctype.name)
@@ -50,6 +50,6 @@ def execute():
df = frappe.new_doc('DocField', meta, 'fields')
df.update(cf)
meta.fields.append(df)
- frappe.db.sql('DELETE FROM `tabCustom Field` WHERE name=%s', cf.name)
+ frappe.db.delete("Custom Field", {"name": cf.name})
meta.save()
diff --git a/frappe/patches/v11_0/change_email_signature_fieldtype.py b/frappe/patches/v11_0/change_email_signature_fieldtype.py
index ccfa8541c3..7c57aa044e 100644
--- a/frappe/patches/v11_0/change_email_signature_fieldtype.py
+++ b/frappe/patches/v11_0/change_email_signature_fieldtype.py
@@ -1,5 +1,5 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/patches/v11_0/remove_doctype_user_permissions_for_page_and_report.py b/frappe/patches/v11_0/remove_doctype_user_permissions_for_page_and_report.py
index 5c54b1e5c1..ff5cf3fc5e 100644
--- a/frappe/patches/v11_0/remove_doctype_user_permissions_for_page_and_report.py
+++ b/frappe/patches/v11_0/remove_doctype_user_permissions_for_page_and_report.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/patches/v11_0/remove_skip_for_doctype.py b/frappe/patches/v11_0/remove_skip_for_doctype.py
index 638a5a0fd7..1bbe74bb6d 100644
--- a/frappe/patches/v11_0/remove_skip_for_doctype.py
+++ b/frappe/patches/v11_0/remove_skip_for_doctype.py
@@ -2,6 +2,7 @@
import frappe
from frappe.desk.form.linked_with import get_linked_doctypes
from frappe.patches.v11_0.replicate_old_user_permissions import get_doctypes_to_skip
+from frappe.query_builder import Field
# `skip_for_doctype` was a un-normalized way of storing for which
# doctypes the user permission was applicable.
@@ -72,16 +73,12 @@ def execute():
frappe.db.set_value('User Permission', user_permission.name, 'apply_to_all_doctypes', 1)
if new_user_permissions_list:
- frappe.db.sql('''
- INSERT INTO `tabUser Permission`
- (`name`, `user`, `allow`, `for_value`, `applicable_for`, `apply_to_all_doctypes`, `creation`, `modified`)
- VALUES {}
- '''.format( # nosec
- ', '.join(['%s'] * len(new_user_permissions_list))
- ), tuple(new_user_permissions_list))
+ frappe.qb.into("User Permission").columns(
+ "name", "user", "allow", "for_value", "applicable_for", "apply_to_all_doctypes", "creation", "modified"
+ ).insert(*new_user_permissions_list).run()
if user_permissions_to_delete:
- frappe.db.sql('DELETE FROM `tabUser Permission` WHERE `name` in ({})' # nosec
- .format(','.join(['%s'] * len(user_permissions_to_delete))),
- tuple(user_permissions_to_delete)
+ frappe.db.delete(
+ "User Permission",
+ filters=(Field("name").isin(tuple(user_permissions_to_delete)))
)
diff --git a/frappe/patches/v11_0/sync_stripe_settings_before_migrate.py b/frappe/patches/v11_0/sync_stripe_settings_before_migrate.py
index a8e9bd4de1..901ab66bfd 100644
--- a/frappe/patches/v11_0/sync_stripe_settings_before_migrate.py
+++ b/frappe/patches/v11_0/sync_stripe_settings_before_migrate.py
@@ -17,4 +17,4 @@ def execute():
settings.secret_key = secret_key
settings.save(ignore_permissions=True)
- frappe.db.sql("""DELETE FROM tabSingles WHERE doctype='Stripe Settings'""")
\ No newline at end of file
+ frappe.db.delete("Singles", {"doctype": "Stripe Settings"})
diff --git a/frappe/patches/v11_0/sync_user_permission_doctype_before_migrate.py b/frappe/patches/v11_0/sync_user_permission_doctype_before_migrate.py
index 55a7b74f7e..6b7a7695f6 100644
--- a/frappe/patches/v11_0/sync_user_permission_doctype_before_migrate.py
+++ b/frappe/patches/v11_0/sync_user_permission_doctype_before_migrate.py
@@ -1,7 +1,7 @@
-
import frappe
+
def execute():
frappe.flags.in_patch = True
- frappe.reload_doc('core', 'doctype', 'user_permission')
+ frappe.reload_doc("core", "doctype", "user_permission")
frappe.db.commit()
diff --git a/frappe/patches/v12_0/delete_feedback_request_if_exists.py b/frappe/patches/v12_0/delete_feedback_request_if_exists.py
index fdbcecfc5a..c1bf46b14a 100644
--- a/frappe/patches/v12_0/delete_feedback_request_if_exists.py
+++ b/frappe/patches/v12_0/delete_feedback_request_if_exists.py
@@ -2,7 +2,4 @@
import frappe
def execute():
- frappe.db.sql('''
- DELETE from `tabDocType`
- WHERE name = 'Feedback Request'
- ''')
\ No newline at end of file
+ frappe.db.delete("DocType", {"name": "Feedback Request"})
diff --git a/frappe/patches/v12_0/remove_deprecated_fields_from_doctype.py b/frappe/patches/v12_0/remove_deprecated_fields_from_doctype.py
index 60599066e6..9c9a79ccbf 100644
--- a/frappe/patches/v12_0/remove_deprecated_fields_from_doctype.py
+++ b/frappe/patches/v12_0/remove_deprecated_fields_from_doctype.py
@@ -8,7 +8,6 @@ def execute():
'DocType': ['hide_heading', 'image_view', 'read_only_onload']
}, delete=1)
- frappe.db.sql('''
- DELETE from `tabProperty Setter`
- WHERE property = 'read_only_onload'
- ''')
+ frappe.db.delete("Property Setter", {
+ "property": "read_only_onload"
+ })
\ No newline at end of file
diff --git a/frappe/patches/v12_0/set_correct_assign_value_in_docs.py b/frappe/patches/v12_0/set_correct_assign_value_in_docs.py
index 65a635c170..5aaadd00e8 100644
--- a/frappe/patches/v12_0/set_correct_assign_value_in_docs.py
+++ b/frappe/patches/v12_0/set_correct_assign_value_in_docs.py
@@ -1,32 +1,27 @@
import frappe
+from frappe.query_builder.functions import GroupConcat, Coalesce
def execute():
- frappe.reload_doc('desk', 'doctype', 'todo')
+ frappe.reload_doc("desk", "doctype", "todo")
- query = '''
- SELECT
- name, reference_type, reference_name, {} as assignees
- FROM
- `tabToDo`
- WHERE
- COALESCE(reference_type, '') != '' AND
- COALESCE(reference_name, '') != '' AND
- status != 'Cancelled'
- GROUP BY
- reference_type, reference_name
- '''
+ ToDo = frappe.qb.DocType("ToDo")
+ assignees = GroupConcat("owner").distinct().as_("assignees")
- assignments = frappe.db.multisql({
- 'mariadb': query.format('GROUP_CONCAT(DISTINCT `owner`)'),
- 'postgres': query.format('STRING_AGG(DISTINCT "owner", ",")')
- }, as_dict=True)
+ assignments = (
+ frappe.qb.from_(ToDo)
+ .select(ToDo.name, ToDo.reference_type, assignees)
+ .where(Coalesce(ToDo.reference_type, "") != "")
+ .where(Coalesce(ToDo.reference_name, "") != "")
+ .where(ToDo.status != "Cancelled")
+ .groupby(ToDo.reference_type, ToDo.reference_name)
+ ).run(as_dict=True)
for doc in assignments:
- assignments = doc.assignees.split(',')
+ assignments = doc.assignees.split(",")
frappe.db.set_value(
doc.reference_type,
doc.reference_name,
- '_assign',
+ "_assign",
frappe.as_json(assignments),
update_modified=False
- )
+ )
\ No newline at end of file
diff --git a/frappe/patches/v12_0/set_default_password_reset_limit.py b/frappe/patches/v12_0/set_default_password_reset_limit.py
index 188f2383e7..e403b5251e 100644
--- a/frappe/patches/v12_0/set_default_password_reset_limit.py
+++ b/frappe/patches/v12_0/set_default_password_reset_limit.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/patches/v12_0/set_primary_key_in_series.py b/frappe/patches/v12_0/set_primary_key_in_series.py
index e5ed2204ba..83a903fc2d 100644
--- a/frappe/patches/v12_0/set_primary_key_in_series.py
+++ b/frappe/patches/v12_0/set_primary_key_in_series.py
@@ -1,21 +1,24 @@
import frappe
def execute():
- #if current = 0, simply delete the key as it'll be recreated on first entry
- frappe.db.sql('delete from `tabSeries` where current = 0')
- duplicate_keys = frappe.db.sql('''
- SELECT name, max(current) as current
- from
- `tabSeries`
- group by
- name
- having count(name) > 1
- ''', as_dict=True)
- for row in duplicate_keys:
- frappe.db.sql('delete from `tabSeries` where name = %(key)s', {
- 'key': row.name
- })
- if row.current:
- frappe.db.sql('insert into `tabSeries`(`name`, `current`) values (%(name)s, %(current)s)', row)
- frappe.db.commit()
- frappe.db.sql('ALTER table `tabSeries` ADD PRIMARY KEY IF NOT EXISTS (name)')
+ #if current = 0, simply delete the key as it'll be recreated on first entry
+ frappe.db.delete("Series", {"current": 0})
+
+ duplicate_keys = frappe.db.sql('''
+ SELECT name, max(current) as current
+ from
+ `tabSeries`
+ group by
+ name
+ having count(name) > 1
+ ''', as_dict=True)
+
+ for row in duplicate_keys:
+ frappe.db.delete("Series", {
+ "name": row.name
+ })
+ if row.current:
+ frappe.db.sql('insert into `tabSeries`(`name`, `current`) values (%(name)s, %(current)s)', row)
+ frappe.db.commit()
+
+ frappe.db.sql('ALTER table `tabSeries` ADD PRIMARY KEY IF NOT EXISTS (name)')
diff --git a/frappe/patches/v12_0/setup_comments_from_communications.py b/frappe/patches/v12_0/setup_comments_from_communications.py
index 039ceeff35..11e02965f1 100644
--- a/frappe/patches/v12_0/setup_comments_from_communications.py
+++ b/frappe/patches/v12_0/setup_comments_from_communications.py
@@ -29,4 +29,6 @@ def execute():
frappe.db.auto_commit_on_many_writes = False
# clean up
- frappe.db.sql("delete from `tabCommunication` where communication_type = 'Comment'")
+ frappe.db.delete("Communication", {
+ "communication_type": "Comment"
+ })
diff --git a/frappe/patches/v13_0/delete_event_producer_and_consumer_keys.py b/frappe/patches/v13_0/delete_event_producer_and_consumer_keys.py
index 776e9c796e..2d9e232da5 100644
--- a/frappe/patches/v13_0/delete_event_producer_and_consumer_keys.py
+++ b/frappe/patches/v13_0/delete_event_producer_and_consumer_keys.py
@@ -1,5 +1,5 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/patches/v13_0/delete_package_publish_tool.py b/frappe/patches/v13_0/delete_package_publish_tool.py
index bf9aaf5a76..5c1678bdbe 100644
--- a/frappe/patches/v13_0/delete_package_publish_tool.py
+++ b/frappe/patches/v13_0/delete_package_publish_tool.py
@@ -1,5 +1,5 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/patches/v13_0/enable_custom_script.py b/frappe/patches/v13_0/enable_custom_script.py
index 0684074fe7..de027ab97a 100644
--- a/frappe/patches/v13_0/enable_custom_script.py
+++ b/frappe/patches/v13_0/enable_custom_script.py
@@ -1,5 +1,5 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/patches/v13_0/generate_theme_files_in_public_folder.py b/frappe/patches/v13_0/generate_theme_files_in_public_folder.py
index dd9fb1961a..6e8e0d7fc5 100644
--- a/frappe/patches/v13_0/generate_theme_files_in_public_folder.py
+++ b/frappe/patches/v13_0/generate_theme_files_in_public_folder.py
@@ -1,5 +1,5 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/patches/v13_0/increase_password_length.py b/frappe/patches/v13_0/increase_password_length.py
index 1bb1979051..deb7d7e98a 100644
--- a/frappe/patches/v13_0/increase_password_length.py
+++ b/frappe/patches/v13_0/increase_password_length.py
@@ -1,7 +1,4 @@
import frappe
def execute():
- frappe.db.multisql({
- "mariadb": "ALTER TABLE `__Auth` MODIFY `password` TEXT NOT NULL",
- "postgres": 'ALTER TABLE "__Auth" ALTER COLUMN "password" TYPE TEXT'
- })
+ frappe.db.change_column_type("__Auth", column="password", type="TEXT")
diff --git a/frappe/patches/v13_0/jinja_hook.py b/frappe/patches/v13_0/jinja_hook.py
index 990ae50f35..e1c9175576 100644
--- a/frappe/patches/v13_0/jinja_hook.py
+++ b/frappe/patches/v13_0/jinja_hook.py
@@ -1,5 +1,5 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from click import secho
diff --git a/frappe/patches/v13_0/queryreport_columns.py b/frappe/patches/v13_0/queryreport_columns.py
index 5c381f4f3e..ed22ce4441 100644
--- a/frappe/patches/v13_0/queryreport_columns.py
+++ b/frappe/patches/v13_0/queryreport_columns.py
@@ -1,5 +1,5 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
import json
diff --git a/frappe/patches/v13_0/remove_tailwind_from_page_builder.py b/frappe/patches/v13_0/remove_tailwind_from_page_builder.py
index 2bf2c7bf87..b26d2bef4a 100644
--- a/frappe/patches/v13_0/remove_tailwind_from_page_builder.py
+++ b/frappe/patches/v13_0/remove_tailwind_from_page_builder.py
@@ -1,5 +1,5 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/patches/v13_0/remove_twilio_settings.py b/frappe/patches/v13_0/remove_twilio_settings.py
index 363cbdd4b6..826edfb951 100644
--- a/frappe/patches/v13_0/remove_twilio_settings.py
+++ b/frappe/patches/v13_0/remove_twilio_settings.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
@@ -12,7 +12,9 @@ def execute():
frappe.delete_doc_if_exists('DocType', 'Twilio Number Group')
if twilio_settings_doctype_in_integrations():
frappe.delete_doc_if_exists('DocType', 'Twilio Settings')
- frappe.db.sql("delete from `tabSingles` where `doctype`=%s", 'Twilio Settings')
+ frappe.db.delete("Singles", {
+ "doctype": "Twilio Settings"
+ })
def twilio_settings_doctype_in_integrations() -> bool:
"""Check Twilio Settings doctype exists in integrations module or not.
diff --git a/frappe/patches/v13_0/rename_list_view_setting_to_list_view_settings.py b/frappe/patches/v13_0/rename_list_view_setting_to_list_view_settings.py
index 3122de8bea..db3ab1b32a 100644
--- a/frappe/patches/v13_0/rename_list_view_setting_to_list_view_settings.py
+++ b/frappe/patches/v13_0/rename_list_view_setting_to_list_view_settings.py
@@ -1,5 +1,5 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/patches/v13_0/rename_notification_fields.py b/frappe/patches/v13_0/rename_notification_fields.py
index 1413d80358..2f314df9c1 100644
--- a/frappe/patches/v13_0/rename_notification_fields.py
+++ b/frappe/patches/v13_0/rename_notification_fields.py
@@ -1,5 +1,5 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.utils.rename_field import rename_field
diff --git a/frappe/patches/v13_0/rename_onboarding.py b/frappe/patches/v13_0/rename_onboarding.py
index 852065dfd2..cd910195ad 100644
--- a/frappe/patches/v13_0/rename_onboarding.py
+++ b/frappe/patches/v13_0/rename_onboarding.py
@@ -1,5 +1,5 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/patches/v13_0/replace_old_data_import.py b/frappe/patches/v13_0/replace_old_data_import.py
index 838881b48e..7d2692a433 100644
--- a/frappe/patches/v13_0/replace_old_data_import.py
+++ b/frappe/patches/v13_0/replace_old_data_import.py
@@ -1,5 +1,5 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/patches/v13_0/update_duration_options.py b/frappe/patches/v13_0/update_duration_options.py
index e0d8dea4ea..48f0dc0969 100644
--- a/frappe/patches/v13_0/update_duration_options.py
+++ b/frappe/patches/v13_0/update_duration_options.py
@@ -1,5 +1,5 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/patches/v13_0/update_newsletter_content_type.py b/frappe/patches/v13_0/update_newsletter_content_type.py
index 5f047680ee..39758c8257 100644
--- a/frappe/patches/v13_0/update_newsletter_content_type.py
+++ b/frappe/patches/v13_0/update_newsletter_content_type.py
@@ -1,5 +1,5 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/patches/v13_0/update_notification_channel_if_empty.py b/frappe/patches/v13_0/update_notification_channel_if_empty.py
index bcf9a7b28c..43cf813c74 100644
--- a/frappe/patches/v13_0/update_notification_channel_if_empty.py
+++ b/frappe/patches/v13_0/update_notification_channel_if_empty.py
@@ -1,5 +1,5 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/patches/v13_0/web_template_set_module.py b/frappe/patches/v13_0/web_template_set_module.py
index 2ee9e3ba2d..d200f4e0da 100644
--- a/frappe/patches/v13_0/web_template_set_module.py
+++ b/frappe/patches/v13_0/web_template_set_module.py
@@ -1,5 +1,5 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
diff --git a/frappe/patches/v14_0/__init__.py b/frappe/patches/v14_0/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/patches/v14_0/drop_data_import_legacy.py b/frappe/patches/v14_0/drop_data_import_legacy.py
new file mode 100644
index 0000000000..2037930c9f
--- /dev/null
+++ b/frappe/patches/v14_0/drop_data_import_legacy.py
@@ -0,0 +1,22 @@
+import frappe
+import click
+
+
+def execute():
+ doctype = "Data Import Legacy"
+ table = frappe.utils.get_table_name(doctype)
+
+ # delete the doctype record to avoid broken links
+ frappe.db.delete("DocType", {"name": doctype})
+
+ # leaving table in database for manual cleanup
+ click.secho(
+ f"`{doctype}` has been deprecated. The DocType is deleted, but the data still"
+ " exists on the database. If this data is worth recovering, you may export it"
+ f" using\n\n\tbench --site {frappe.local.site} backup -i '{doctype}'\n\nAfter"
+ " this, the table will continue to persist in the database, until you choose"
+ " to remove it yourself. If you want to drop the table, you may run\n\n\tbench"
+ f" --site {frappe.local.site} execute frappe.db.sql --args \"('DROP TABLE IF"
+ f" EXISTS `{table}`', )\"\n",
+ fg="yellow",
+ )
diff --git a/frappe/patches/v14_0/rename_cancelled_documents.py b/frappe/patches/v14_0/rename_cancelled_documents.py
new file mode 100644
index 0000000000..4b565d4f76
--- /dev/null
+++ b/frappe/patches/v14_0/rename_cancelled_documents.py
@@ -0,0 +1,213 @@
+import functools
+import traceback
+
+import frappe
+
+def execute():
+ """Rename cancelled documents by adding a postfix.
+ """
+ rename_cancelled_docs()
+
+def get_submittable_doctypes():
+ """Returns list of submittable doctypes in the system.
+ """
+ return frappe.db.get_all('DocType', filters={'is_submittable': 1}, pluck='name')
+
+def get_cancelled_doc_names(doctype):
+ """Return names of cancelled document names those are in old format.
+ """
+ docs = frappe.db.get_all(doctype, filters={'docstatus': 2}, pluck='name')
+ return [each for each in docs if not (each.endswith('-CANC') or ('-CANC-' in each))]
+
+@functools.lru_cache()
+def get_linked_doctypes():
+ """Returns list of doctypes those are linked with given doctype using 'Link' fieldtype.
+ """
+ filters=[['fieldtype','=', 'Link']]
+ links = frappe.get_all("DocField",
+ fields=["parent", "fieldname", "options as linked_to"],
+ filters=filters,
+ as_list=1)
+
+ links+= frappe.get_all("Custom Field",
+ fields=["dt as parent", "fieldname", "options as linked_to"],
+ filters=filters,
+ as_list=1)
+
+ links_by_doctype = {}
+ for doctype, fieldname, linked_to in links:
+ links_by_doctype.setdefault(linked_to, []).append((doctype, fieldname))
+ return links_by_doctype
+
+@functools.lru_cache()
+def get_single_doctypes():
+ return frappe.get_all("DocType", filters={'issingle': 1}, pluck='name')
+
+@functools.lru_cache()
+def get_dynamic_linked_doctypes():
+ filters=[['fieldtype','=', 'Dynamic Link']]
+
+ # find dynamic links of parents
+ links = frappe.get_all("DocField",
+ fields=["parent as doctype", "fieldname", "options as doctype_fieldname"],
+ filters=filters,
+ as_list=1)
+ links+= frappe.get_all("Custom Field",
+ fields=["dt as doctype", "fieldname", "options as doctype_fieldname"],
+ filters=filters,
+ as_list=1)
+ return links
+
+@functools.lru_cache()
+def get_child_tables():
+ """
+ """
+ filters =[['fieldtype', 'in', ('Table', 'Table MultiSelect')]]
+ links = frappe.get_all("DocField",
+ fields=["parent as doctype", "options as child_table"],
+ filters=filters,
+ as_list=1)
+
+ links+= frappe.get_all("Custom Field",
+ fields=["dt as doctype", "options as child_table"],
+ filters=filters,
+ as_list=1)
+
+ map = {}
+ for doctype, child_table in links:
+ map.setdefault(doctype, []).append(child_table)
+ return map
+
+def update_cancelled_document_names(doctype, cancelled_doc_names):
+ return frappe.db.sql("""
+ update
+ `tab{doctype}`
+ set
+ name=CONCAT(name, '-CANC')
+ where
+ docstatus=2
+ and
+ name in %(cancelled_doc_names)s;
+ """.format(doctype=doctype), {'cancelled_doc_names': cancelled_doc_names})
+
+def update_amended_field(doctype, cancelled_doc_names):
+ return frappe.db.sql("""
+ update
+ `tab{doctype}`
+ set
+ amended_from=CONCAT(amended_from, '-CANC')
+ where
+ amended_from in %(cancelled_doc_names)s;
+ """.format(doctype=doctype), {'cancelled_doc_names': cancelled_doc_names})
+
+def update_attachments(doctype, cancelled_doc_names):
+ frappe.db.sql("""
+ update
+ `tabFile`
+ set
+ attached_to_name=CONCAT(attached_to_name, '-CANC')
+ where
+ attached_to_doctype=%(dt)s and attached_to_name in %(cancelled_doc_names)s
+ """, {'cancelled_doc_names': cancelled_doc_names, 'dt': doctype})
+
+def update_versions(doctype, cancelled_doc_names):
+ frappe.db.sql("""
+ UPDATE
+ `tabVersion`
+ SET
+ docname=CONCAT(docname, '-CANC')
+ WHERE
+ ref_doctype=%(dt)s AND docname in %(cancelled_doc_names)s
+ """, {'cancelled_doc_names': cancelled_doc_names, 'dt': doctype})
+
+def update_linked_doctypes(doctype, cancelled_doc_names):
+ single_doctypes = get_single_doctypes()
+
+ for linked_dt, field in get_linked_doctypes().get(doctype, []):
+ if linked_dt not in single_doctypes:
+ frappe.db.sql("""
+ update
+ `tab{linked_dt}`
+ set
+ `{column}`=CONCAT(`{column}`, '-CANC')
+ where
+ `{column}` in %(cancelled_doc_names)s;
+ """.format(linked_dt=linked_dt, column=field),
+ {'cancelled_doc_names': cancelled_doc_names})
+ else:
+ doc = frappe.get_single(linked_dt)
+ if getattr(doc, field) in cancelled_doc_names:
+ setattr(doc, field, getattr(doc, field)+'-CANC')
+ doc.flags.ignore_mandatory=True
+ doc.flags.ignore_validate=True
+ doc.save(ignore_permissions=True)
+
+def update_dynamic_linked_doctypes(doctype, cancelled_doc_names):
+ single_doctypes = get_single_doctypes()
+
+ for linked_dt, fieldname, doctype_fieldname in get_dynamic_linked_doctypes():
+ if linked_dt not in single_doctypes:
+ frappe.db.sql("""
+ update
+ `tab{linked_dt}`
+ set
+ `{column}`=CONCAT(`{column}`, '-CANC')
+ where
+ `{column}` in %(cancelled_doc_names)s and {doctype_fieldname}=%(dt)s;
+ """.format(linked_dt=linked_dt, column=fieldname, doctype_fieldname=doctype_fieldname),
+ {'cancelled_doc_names': cancelled_doc_names, 'dt': doctype})
+ else:
+ doc = frappe.get_single(linked_dt)
+ if getattr(doc, doctype_fieldname) == doctype and getattr(doc, fieldname) in cancelled_doc_names:
+ setattr(doc, fieldname, getattr(doc, fieldname)+'-CANC')
+ doc.flags.ignore_mandatory=True
+ doc.flags.ignore_validate=True
+ doc.save(ignore_permissions=True)
+
+def update_child_tables(doctype, cancelled_doc_names):
+ child_tables = get_child_tables().get(doctype, [])
+ single_doctypes = get_single_doctypes()
+
+ for table in child_tables:
+ if table not in single_doctypes:
+ frappe.db.sql("""
+ update
+ `tab{table}`
+ set
+ parent=CONCAT(parent, '-CANC')
+ where
+ parenttype=%(dt)s and parent in %(cancelled_doc_names)s;
+ """.format(table=table), {'cancelled_doc_names': cancelled_doc_names, 'dt': doctype})
+ else:
+ doc = frappe.get_single(table)
+ if getattr(doc, 'parenttype')==doctype and getattr(doc, 'parent') in cancelled_doc_names:
+ setattr(doc, 'parent', getattr(doc, 'parent')+'-CANC')
+ doc.flags.ignore_mandatory=True
+ doc.flags.ignore_validate=True
+ doc.save(ignore_permissions=True)
+
+def rename_cancelled_docs():
+ submittable_doctypes = get_submittable_doctypes()
+
+ for dt in submittable_doctypes:
+ for retry in range(2):
+ try:
+ cancelled_doc_names = tuple(get_cancelled_doc_names(dt))
+ if not cancelled_doc_names:
+ break
+ update_cancelled_document_names(dt, cancelled_doc_names)
+ update_amended_field(dt, cancelled_doc_names)
+ update_child_tables(dt, cancelled_doc_names)
+ update_linked_doctypes(dt, cancelled_doc_names)
+ update_dynamic_linked_doctypes(dt, cancelled_doc_names)
+ update_attachments(dt, cancelled_doc_names)
+ update_versions(dt, cancelled_doc_names)
+ print(f"Renaming cancelled records of {dt} doctype")
+ frappe.db.commit()
+ break
+ except Exception:
+ if retry == 1:
+ print(f"Failed to rename the cancelled records of {dt} doctype, moving on!")
+ traceback.print_exc()
+ frappe.db.rollback()
+
diff --git a/frappe/patches/v14_0/update_github_endpoints.py b/frappe/patches/v14_0/update_github_endpoints.py
new file mode 100644
index 0000000000..8f9a06a043
--- /dev/null
+++ b/frappe/patches/v14_0/update_github_endpoints.py
@@ -0,0 +1,10 @@
+import frappe
+import json
+
+def execute():
+ if frappe.db.exists("Social Login Key", "github"):
+ frappe.db.set_value("Social Login Key", "github", "auth_url_data",
+ json.dumps({
+ "scope": "user:email"
+ })
+ )
diff --git a/frappe/patches/v14_0/update_workspace2.py b/frappe/patches/v14_0/update_workspace2.py
new file mode 100644
index 0000000000..82076c4328
--- /dev/null
+++ b/frappe/patches/v14_0/update_workspace2.py
@@ -0,0 +1,69 @@
+import frappe
+import json
+from frappe import _
+
+def execute():
+ frappe.reload_doc('desk', 'doctype', 'workspace', force=True)
+
+ for seq, wspace in enumerate(frappe.get_all('Workspace', order_by='name asc')):
+ doc = frappe.get_doc('Workspace', wspace.name)
+ content = create_content(doc)
+ update_wspace(doc, seq, content)
+ frappe.db.commit()
+
+def create_content(doc):
+ content = []
+ if doc.onboarding:
+ content.append({"type":"onboarding","data":{"onboarding_name":doc.onboarding,"col":12}})
+ if doc.charts:
+ invalid_links = []
+ for c in doc.charts:
+ if c.get_invalid_links()[0]:
+ invalid_links.append(c)
+ else:
+ content.append({"type":"chart","data":{"chart_name":c.label,"col":12}})
+ for l in invalid_links:
+ del doc.charts[doc.charts.index(l)]
+ if doc.shortcuts:
+ invalid_links = []
+ if doc.charts:
+ content.append({"type":"spacer","data":{"col":12}})
+ content.append({"type":"header","data":{"text":doc.shortcuts_label or _("Your Shortcuts"),"level":4,"col":12}})
+ for s in doc.shortcuts:
+ if s.get_invalid_links()[0]:
+ invalid_links.append(s)
+ else:
+ content.append({"type":"shortcut","data":{"shortcut_name":s.label,"col":4}})
+ for l in invalid_links:
+ del doc.shortcuts[doc.shortcuts.index(l)]
+ if doc.links:
+ invalid_links = []
+ content.append({"type":"spacer","data":{"col":12}})
+ content.append({"type":"header","data":{"text":doc.cards_label or _("Reports & Masters"),"level":4,"col":12}})
+ for l in doc.links:
+ if l.type == 'Card Break':
+ content.append({"type":"card","data":{"card_name":l.label,"col":4}})
+ if l.get_invalid_links()[0]:
+ invalid_links.append(l)
+ for l in invalid_links:
+ del doc.links[doc.links.index(l)]
+ return content
+
+def update_wspace(doc, seq, content):
+ if not doc.title and not doc.content and not doc.is_standard and not doc.public:
+ doc.sequence_id = seq + 1
+ doc.content = json.dumps(content)
+ doc.public = 0 if doc.for_user else 1
+ doc.title = doc.extends or doc.label
+ doc.extends = ''
+ doc.category = ''
+ doc.onboarding = ''
+ doc.extends_another_page = 0
+ doc.is_default = 0
+ doc.is_standard = 0
+ doc.developer_mode_only = 0
+ doc.disable_user_customization = 0
+ doc.pin_to_top = 0
+ doc.pin_to_bottom = 0
+ doc.hide_custom = 0
+ doc.save(ignore_permissions=True)
\ No newline at end of file
diff --git a/frappe/permissions.py b/frappe/permissions.py
index 07b4a2e68f..96e1910462 100644
--- a/frappe/permissions.py
+++ b/frappe/permissions.py
@@ -1,15 +1,17 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import copy
import frappe
import frappe.share
from frappe import _, msgprint
from frappe.utils import cint
+from frappe.query_builder import DocType
rights = ("select", "read", "write", "create", "delete", "submit", "cancel", "amend",
"print", "email", "report", "import", "export", "set_user_permissions", "share")
+
def check_admin_or_system_manager(user=None):
if not user: user = frappe.session.user
@@ -32,7 +34,7 @@ def print_has_permission_check_logs(func):
return inner
@print_has_permission_check_logs
-def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None, raise_exception=True):
+def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None, raise_exception=True, parent_doctype=None):
"""Returns True if user has permission `ptype` for given `doctype`.
If `doc` is passed, it also checks user, share and owner permissions.
@@ -45,11 +47,12 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None, ra
doc = doctype
doctype = doc.doctype
- if frappe.is_table(doctype):
+ if user == "Administrator":
return True
- if user=="Administrator":
- return True
+ if frappe.is_table(doctype):
+ return has_child_table_permission(doctype, ptype, doc, verbose,
+ user, raise_exception, parent_doctype)
meta = frappe.get_meta(doctype)
@@ -94,7 +97,7 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None, ra
if not perm:
perm = false_if_not_shared()
- return perm
+ return bool(perm)
def get_doc_permissions(doc, user=None, ptype=None):
"""Returns a dict of evaluated permissions for given `doc` like `{"read":1, "write":1}`"""
@@ -105,13 +108,9 @@ def get_doc_permissions(doc, user=None, ptype=None):
meta = frappe.get_meta(doc.doctype)
def is_user_owner():
- doc_owner = doc.get('owner') or ''
- doc_owner = doc_owner.lower()
- session_user = frappe.session.user.lower()
- return doc_owner == session_user
+ return (doc.get("owner") or "").lower() == frappe.session.user.lower()
-
- if has_controller_permissions(doc, ptype, user=user) == False :
+ if has_controller_permissions(doc, ptype, user=user) is False:
push_perm_check_log('Not allowed via controller permission check')
return {ptype: 0}
@@ -180,22 +179,23 @@ def get_role_permissions(doctype_meta, user=None, is_owner=None):
applicable_permissions = list(filter(is_perm_applicable, getattr(doctype_meta, 'permissions', [])))
has_if_owner_enabled = any(p.get('if_owner', 0) for p in applicable_permissions)
-
perms['has_if_owner_enabled'] = has_if_owner_enabled
for ptype in rights:
pvalue = any(p.get(ptype, 0) for p in applicable_permissions)
# check if any perm object allows perm type
perms[ptype] = cint(pvalue)
- if (pvalue
- and has_if_owner_enabled
- and not has_permission_without_if_owner_enabled(ptype)
- and ptype != 'create'):
+ if (
+ pvalue
+ and has_if_owner_enabled
+ and not has_permission_without_if_owner_enabled(ptype)
+ and ptype != 'create'
+ ):
perms['if_owner'][ptype] = cint(pvalue and is_owner)
# has no access if not owner
# only provide select or read access so that user is able to at-least access list
# (and the documents will be filtered based on owner sin further checks)
- perms[ptype] = 1 if ptype in ['select', 'read'] else 0
+ perms[ptype] = 1 if ptype in ('select', 'read') else 0
frappe.local.role_permissions[cache_key] = perms
@@ -299,7 +299,7 @@ def has_controller_permissions(doc, ptype, user=None):
if not methods:
return None
- for method in methods:
+ for method in reversed(methods):
controller_permission = frappe.call(frappe.get_attr(method), doc=doc, ptype=ptype, user=user)
if controller_permission is not None:
return controller_permission
@@ -331,8 +331,7 @@ def get_all_perms(role):
'''Returns valid permissions for a given role'''
perms = frappe.get_all('DocPerm', fields='*', filters=dict(role=role))
custom_perms = frappe.get_all('Custom DocPerm', fields='*', filters=dict(role=role))
- doctypes_with_custom_perms = frappe.db.sql_list("""select distinct parent
- from `tabCustom DocPerm`""")
+ doctypes_with_custom_perms = frappe.get_all("Custom DocPerm", pluck="parent", distinct=True)
for p in perms:
if p.parent not in doctypes_with_custom_perms:
@@ -349,10 +348,13 @@ def get_roles(user=None, with_standard=True):
def get():
if user == 'Administrator':
- return [r[0] for r in frappe.db.sql("select name from `tabRole`")] # return all available roles
+ return frappe.get_all("Role", pluck="name") # return all available roles
else:
- return [r[0] for r in frappe.db.sql("""select role from `tabHas Role`
- where parent=%s and role not in ('All', 'Guest')""", (user,))] + ['All', 'Guest']
+ table = DocType("Has Role")
+ roles = frappe.qb.from_(table).where(
+ (table.parent == user) & (table.role.notin(["All", "Guest"]))
+ ).select(table.role).run(pluck=True)
+ return roles + ['All', 'Guest']
roles = frappe.cache().hget("roles", user, get)
@@ -461,10 +463,9 @@ def update_permission_property(doctype, role, permlevel, ptype, value=None, vali
name = frappe.get_value('Custom DocPerm', dict(parent=doctype, role=role,
permlevel=permlevel))
+ table = DocType("Custom DocPerm")
+ frappe.qb.update(table).set(ptype, value).where(table.name == name).run()
- frappe.db.sql("""
- update `tabCustom DocPerm`
- set `{0}`=%s where name=%s""".format(ptype), (value, name))
if validate:
validate_permissions_for_doctype(doctype)
@@ -516,8 +517,7 @@ def reset_perms(doctype):
"""Reset permissions for given doctype."""
from frappe.desk.notifications import delete_notification_count_for
delete_notification_count_for(doctype)
-
- frappe.db.sql("""delete from `tabCustom DocPerm` where parent=%s""", doctype)
+ frappe.db.delete("Custom DocPerm", {"parent": doctype})
def get_linked_doctypes(dt):
return list(set([dt] + [d.options for d in
@@ -561,3 +561,35 @@ def filter_allowed_docs_for_doctype(user_permissions, doctype, with_default_doc=
def push_perm_check_log(log):
if frappe.flags.get('has_permission_check_logs') == None: return
frappe.flags.get('has_permission_check_logs').append(_(log))
+
+def has_child_table_permission(child_doctype, ptype="read", child_doc=None,
+ verbose=False, user=None, raise_exception=True, parent_doctype=None):
+ parent_doc = None
+
+ if child_doc:
+ parent_doctype = child_doc.get("parenttype")
+ parent_doc = frappe.get_cached_doc({
+ "doctype": parent_doctype,
+ "docname": child_doc.get("parent")
+ })
+
+ if parent_doctype:
+ if not is_parent_valid(child_doctype, parent_doctype):
+ frappe.throw(_("{0} is not a valid parent DocType for {1}").format(
+ frappe.bold(parent_doctype),
+ frappe.bold(child_doctype)
+ ), title=_("Invalid Parent DocType"))
+ else:
+ frappe.throw(_("Please specify a valid parent DocType for {0}").format(
+ frappe.bold(child_doctype)
+ ), title=_("Parent DocType Required"))
+
+ return has_permission(parent_doctype, ptype=ptype, doc=parent_doc,
+ verbose=verbose, user=user, raise_exception=raise_exception)
+
+
+def is_parent_valid(child_doctype, parent_doctype):
+ from frappe.core.utils import find
+ parent_meta = frappe.get_meta(parent_doctype)
+ child_table_field_exists = find(parent_meta.get_table_fields(), lambda d: d.options == child_doctype)
+ return not parent_meta.istable and child_table_field_exists
\ No newline at end of file
diff --git a/frappe/printing/doctype/letter_head/letter_head.json b/frappe/printing/doctype/letter_head/letter_head.json
index f6c9def567..f723a6b489 100644
--- a/frappe/printing/doctype/letter_head/letter_head.json
+++ b/frappe/printing/doctype/letter_head/letter_head.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_rename": 1,
"autoname": "field:letter_head_name",
"creation": "2012-11-22 17:45:46",
@@ -13,6 +14,9 @@
"is_default",
"letter_head_image_section",
"image",
+ "image_height",
+ "image_width",
+ "align",
"header_section",
"content",
"footer_section",
@@ -100,15 +104,34 @@
"fieldname": "footer",
"fieldtype": "HTML Editor",
"label": "Footer HTML"
+ },
+ {
+ "default": "Left",
+ "fieldname": "align",
+ "fieldtype": "Select",
+ "label": "Align",
+ "options": "Left\nRight\nCenter"
+ },
+ {
+ "fieldname": "image_height",
+ "fieldtype": "Float",
+ "label": "Image Height"
+ },
+ {
+ "fieldname": "image_width",
+ "fieldtype": "Float",
+ "label": "Image Width"
}
],
"icon": "fa fa-font",
"idx": 1,
+ "links": [],
"max_attachments": 3,
- "modified": "2019-11-11 18:46:43.375120",
+ "modified": "2021-10-03 14:37:58.314696",
"modified_by": "Administrator",
"module": "Printing",
"name": "Letter Head",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/printing/doctype/letter_head/letter_head.py b/frappe/printing/doctype/letter_head/letter_head.py
index 948be60b88..67c0d236e0 100644
--- a/frappe/printing/doctype/letter_head/letter_head.py
+++ b/frappe/printing/doctype/letter_head/letter_head.py
@@ -1,8 +1,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# License: MIT. See LICENSE
import frappe
-from frappe.utils import is_image
+from frappe.utils import is_image, flt
from frappe.model.document import Document
from frappe import _
@@ -26,7 +26,15 @@ class LetterHead(Document):
def set_image(self):
if self.source=='Image':
if self.image and is_image(self.image):
- self.content = '
'.format(self.image)
+ self.image_width = flt(self.image_width)
+ self.image_height = flt(self.image_height)
+ dimension = 'width' if self.image_width > self.image_height else 'height'
+ dimension_value = self.get('image_' + dimension)
+ self.content = f'''
+
+

+
+ '''
frappe.msgprint(frappe._('Header HTML set from attachment {0}').format(self.image), alert = True)
else:
frappe.msgprint(frappe._('Please attach an image file to set HTML'), alert = True, indicator = 'orange')
diff --git a/frappe/printing/doctype/letter_head/test_letter_head.py b/frappe/printing/doctype/letter_head/test_letter_head.py
index 96dfc68705..67d307ee8b 100644
--- a/frappe/printing/doctype/letter_head/test_letter_head.py
+++ b/frappe/printing/doctype/letter_head/test_letter_head.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/printing/doctype/network_printer_settings/__init__.py b/frappe/printing/doctype/network_printer_settings/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/printing/doctype/network_printer_settings/network_printer_settings.js b/frappe/printing/doctype/network_printer_settings/network_printer_settings.js
new file mode 100644
index 0000000000..043afd388f
--- /dev/null
+++ b/frappe/printing/doctype/network_printer_settings/network_printer_settings.js
@@ -0,0 +1,29 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Network Printer Settings', {
+ onload (frm) {
+ frm.trigger("connect_print_server");
+ },
+ server_ip (frm) {
+ frm.trigger("connect_print_server");
+ },
+ port (frm) {
+ frm.trigger("connect_print_server");
+ },
+ connect_print_server (frm) {
+ if (frm.doc.server_ip && frm.doc.port) {
+ frappe.call({
+ "doc": frm.doc,
+ "method": "get_printers_list",
+ "args": {
+ ip: frm.doc.server_ip,
+ port: frm.doc.port
+ },
+ callback: function(data) {
+ frm.set_df_property('printer_name', 'options', [""].concat(data.message));
+ }
+ });
+ }
+ }
+});
diff --git a/frappe/printing/doctype/network_printer_settings/network_printer_settings.json b/frappe/printing/doctype/network_printer_settings/network_printer_settings.json
new file mode 100644
index 0000000000..11f1382225
--- /dev/null
+++ b/frappe/printing/doctype/network_printer_settings/network_printer_settings.json
@@ -0,0 +1,76 @@
+{
+ "actions": [],
+ "autoname": "Prompt",
+ "creation": "2021-09-17 11:26:06.943999",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "server_ip",
+ "port",
+ "column_break_4",
+ "printer_name"
+ ],
+ "fields": [
+ {
+ "default": "localhost",
+ "fieldname": "server_ip",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Server IP",
+ "reqd": 1
+ },
+ {
+ "default": "631",
+ "fieldname": "port",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Port",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "printer_name",
+ "fieldtype": "Select",
+ "label": "Printer Name",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-10-07 11:23:13.799402",
+ "modified_by": "Administrator",
+ "module": "Printing",
+ "name": "Network Printer Settings",
+ "naming_rule": "Set by user",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "All",
+ "share": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/printing/doctype/network_printer_settings/network_printer_settings.py b/frappe/printing/doctype/network_printer_settings/network_printer_settings.py
new file mode 100644
index 0000000000..e42ed818c7
--- /dev/null
+++ b/frappe/printing/doctype/network_printer_settings/network_printer_settings.py
@@ -0,0 +1,37 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.model.document import Document
+from frappe import _
+
+class NetworkPrinterSettings(Document):
+ @frappe.whitelist()
+ def get_printers_list(self,ip="localhost",port=631):
+ printer_list = []
+ try:
+ import cups
+ except ImportError:
+ frappe.throw(_('''This feature can not be used as dependencies are missing.
+ Please contact your system manager to enable this by installing pycups!'''))
+ return
+ try:
+ cups.setServer(self.server_ip)
+ cups.setPort(self.port)
+ conn = cups.Connection()
+ printers = conn.getPrinters()
+ for printer_id,printer in printers.items():
+ printer_list.append({
+ 'value': printer_id,
+ 'label': printer['printer-make-and-model']
+ })
+
+ except RuntimeError:
+ frappe.throw(_("Failed to connect to server"))
+ except frappe.ValidationError:
+ frappe.throw(_("Failed to connect to server"))
+ return printer_list
+
+@frappe.whitelist()
+def get_network_printer_settings():
+ return frappe.db.get_list('Network Printer Settings', pluck='name')
diff --git a/frappe/printing/doctype/network_printer_settings/test_network_printer_settings.py b/frappe/printing/doctype/network_printer_settings/test_network_printer_settings.py
new file mode 100644
index 0000000000..86509b239f
--- /dev/null
+++ b/frappe/printing/doctype/network_printer_settings/test_network_printer_settings.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+class TestNetworkPrinterSettings(unittest.TestCase):
+ pass
diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js
index 786f8f97ab..3fd1d9d148 100644
--- a/frappe/printing/doctype/print_format/print_format.js
+++ b/frappe/printing/doctype/print_format/print_format.js
@@ -30,27 +30,33 @@ frappe.ui.form.on("Print Format", {
frappe.msgprint(__("Please select DocType first"));
return;
}
- frappe.set_route("print-format-builder", frm.doc.name);
+ if (frm.doc.print_format_builder_beta) {
+ frappe.set_route("print-format-builder-beta", frm.doc.name);
+ } else {
+ frappe.set_route("print-format-builder", frm.doc.name);
+ }
});
}
else if (frm.doc.custom_format && !frm.doc.raw_printing) {
frm.set_df_property("html", "reqd", 1);
}
- frappe.db.get_value('DocType', frm.doc.doc_type, 'default_print_format', (r) => {
- if (r.default_print_format != frm.doc.name) {
- frm.add_custom_button(__("Set as Default"), function () {
- frappe.call({
- method: "frappe.printing.doctype.print_format.print_format.make_default",
- args: {
- name: frm.doc.name
- },
- callback: function() {
- frm.refresh();
- }
+ if (frappe.model.can_read(frm.doc.doc_type)) {
+ frappe.db.get_value('DocType', frm.doc.doc_type, 'default_print_format', (r) => {
+ if (r.default_print_format != frm.doc.name) {
+ frm.add_custom_button(__("Set as Default"), function () {
+ frappe.call({
+ method: "frappe.printing.doctype.print_format.print_format.make_default",
+ args: {
+ name: frm.doc.name
+ },
+ callback: function() {
+ frm.refresh();
+ }
+ });
});
- });
- }
- });
+ }
+ });
+ }
}
},
custom_format: function (frm) {
diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json
index 4032cef209..75ec0fa7fd 100644
--- a/frappe/printing/doctype/print_format/print_format.json
+++ b/frappe/printing/doctype/print_format/print_format.json
@@ -19,19 +19,26 @@
"html",
"raw_commands",
"section_break_9",
+ "margin_top",
+ "margin_bottom",
+ "margin_left",
+ "margin_right",
"align_labels_right",
"show_section_headings",
"line_breaks",
"absolute_value",
"column_break_11",
+ "font_size",
"font",
+ "page_number",
"css_section",
"css",
"custom_html_help",
"section_break_13",
"print_format_help",
"format_data",
- "print_format_builder"
+ "print_format_builder",
+ "print_format_builder_beta"
],
"fields": [
{
@@ -149,12 +156,10 @@
"options": "Language"
},
{
- "default": "Default",
"depends_on": "eval:!doc.custom_format",
"fieldname": "font",
- "fieldtype": "Select",
- "label": "Font",
- "options": "Default\nHelvetica Neue\nArial\nHelvetica\nVerdana\nMonospace"
+ "fieldtype": "Data",
+ "label": "Google Font"
},
{
"depends_on": "eval:!doc.raw_printing",
@@ -205,16 +210,60 @@
"fieldname": "absolute_value",
"fieldtype": "Check",
"label": "Show Absolute Values"
+ },
+ {
+ "default": "0",
+ "fieldname": "print_format_builder_beta",
+ "fieldtype": "Check",
+ "label": "Print Format Builder Beta"
+ },
+ {
+ "default": "15",
+ "fieldname": "margin_top",
+ "fieldtype": "Float",
+ "label": "Margin Top"
+ },
+ {
+ "default": "15",
+ "fieldname": "margin_bottom",
+ "fieldtype": "Float",
+ "label": "Margin Bottom"
+ },
+ {
+ "default": "15",
+ "fieldname": "margin_left",
+ "fieldtype": "Float",
+ "label": "Margin Left"
+ },
+ {
+ "default": "15",
+ "fieldname": "margin_right",
+ "fieldtype": "Float",
+ "label": "Margin Right"
+ },
+ {
+ "default": "14",
+ "fieldname": "font_size",
+ "fieldtype": "Int",
+ "label": "Font Size"
+ },
+ {
+ "default": "Hide",
+ "fieldname": "page_number",
+ "fieldtype": "Select",
+ "label": "Page Number",
+ "options": "Hide\nTop Left\nTop Center\nTop Right\nBottom Left\nBottom Center\nBottom Right"
}
],
"icon": "fa fa-print",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-03-01 15:25:46.578863",
+ "modified": "2021-10-12 17:52:41.167107",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Format",
+ "naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/printing/doctype/print_format/print_format.py b/frappe/printing/doctype/print_format/print_format.py
index 5d4ff92fe2..f19c0af9bf 100644
--- a/frappe/printing/doctype/print_format/print_format.py
+++ b/frappe/printing/doctype/print_format/print_format.py
@@ -1,16 +1,30 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
import frappe.utils
import json
from frappe import _
from frappe.utils.jinja import validate_template
-
+from frappe.utils.weasyprint import get_html, download_pdf
from frappe.model.document import Document
class PrintFormat(Document):
+ def onload(self):
+ templates = frappe.db.get_all(
+ "Print Format Field Template",
+ fields=["template", "field", "name"],
+ filters={"document_type": self.doc_type},
+ )
+ self.set_onload("print_templates", templates)
+
+ def get_html(self, docname, letterhead=None):
+ return get_html(self.doc_type, docname, self.name, letterhead)
+
+ def download_pdf(self, docname, letterhead=None):
+ return download_pdf(self.doc_type, docname, self.name, letterhead)
+
def validate(self):
if (self.standard=="Yes"
and not frappe.local.conf.get("developer_mode")
@@ -38,6 +52,10 @@ class PrintFormat(Document):
def extract_images(self):
from frappe.core.doctype.file.file import extract_images_from_html
+
+ if self.print_format_builder_beta:
+ return
+
if self.format_data:
data = json.loads(self.format_data)
for df in data:
diff --git a/frappe/printing/doctype/print_format/test_print_format.py b/frappe/printing/doctype/print_format/test_print_format.py
index e65eb0183f..564a2c750c 100644
--- a/frappe/printing/doctype/print_format/test_print_format.py
+++ b/frappe/printing/doctype/print_format/test_print_format.py
@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
import re
diff --git a/frappe/printing/doctype/print_format_field_template/__init__.py b/frappe/printing/doctype/print_format_field_template/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/printing/doctype/print_format_field_template/print_format_field_template.js b/frappe/printing/doctype/print_format_field_template/print_format_field_template.js
new file mode 100644
index 0000000000..7fbb0d7359
--- /dev/null
+++ b/frappe/printing/doctype/print_format_field_template/print_format_field_template.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Print Format Field Template', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/printing/doctype/print_format_field_template/print_format_field_template.json b/frappe/printing/doctype/print_format_field_template/print_format_field_template.json
new file mode 100644
index 0000000000..3b79aae7e8
--- /dev/null
+++ b/frappe/printing/doctype/print_format_field_template/print_format_field_template.json
@@ -0,0 +1,101 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "Prompt",
+ "creation": "2021-10-05 14:23:56.508499",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "document_type",
+ "field",
+ "template_file",
+ "column_break_3",
+ "module",
+ "standard",
+ "section_break_5",
+ "template"
+ ],
+ "fields": [
+ {
+ "depends_on": "eval:!doc.multiple",
+ "fieldname": "document_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Document Type",
+ "mandatory_depends_on": "eval:!doc.multiple",
+ "options": "DocType",
+ "reqd": 1
+ },
+ {
+ "fieldname": "field",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Default Template For Field"
+ },
+ {
+ "depends_on": "eval:!doc.standard",
+ "fieldname": "template",
+ "fieldtype": "Code",
+ "label": "Template",
+ "mandatory_depends_on": "eval:!doc.standard",
+ "options": "HTML"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break",
+ "hide_border": 1
+ },
+ {
+ "depends_on": "standard",
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module",
+ "options": "Module Def"
+ },
+ {
+ "default": "0",
+ "fieldname": "standard",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Standard"
+ },
+ {
+ "depends_on": "eval:doc.standard",
+ "fieldname": "template_file",
+ "fieldtype": "Data",
+ "label": "Template File",
+ "mandatory_depends_on": "eval:doc.standard"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-10-19 17:47:59.577949",
+ "modified_by": "Administrator",
+ "module": "Printing",
+ "name": "Print Format Field Template",
+ "naming_rule": "Set by user",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/printing/doctype/print_format_field_template/print_format_field_template.py b/frappe/printing/doctype/print_format_field_template/print_format_field_template.py
new file mode 100644
index 0000000000..b66afdb6b1
--- /dev/null
+++ b/frappe/printing/doctype/print_format_field_template/print_format_field_template.py
@@ -0,0 +1,43 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.model.document import Document
+from frappe import _
+
+
+class PrintFormatFieldTemplate(Document):
+ def validate(self):
+ if self.standard and not (frappe.conf.developer_mode or frappe.flags.in_patch):
+ frappe.throw(_("Enable developer mode to create a standard Print Template"))
+
+ def before_insert(self):
+ self.validate_duplicate()
+
+ def on_update(self):
+ self.validate_duplicate()
+ self.export_doc()
+
+ def validate_duplicate(self):
+ if not self.standard:
+ return
+ if not self.field:
+ return
+
+ filters = {"document_type": self.document_type, "field": self.field}
+ if not self.is_new():
+ filters.update({"name": ("!=", self.name)})
+ result = frappe.db.get_all("Print Format Field Template", filters=filters, limit=1)
+ if result:
+ frappe.throw(
+ _("A template already exists for field {0} of {1}").format(
+ frappe.bold(self.field), frappe.bold(self.document_type)
+ ),
+ frappe.DuplicateEntryError,
+ title=_("Duplicate Entry"),
+ )
+
+ def export_doc(self):
+ from frappe.modules.utils import export_module_json
+
+ export_module_json(self, self.standard, self.module)
diff --git a/frappe/printing/doctype/print_format_field_template/test_print_format_field_template.py b/frappe/printing/doctype/print_format_field_template/test_print_format_field_template.py
new file mode 100644
index 0000000000..f0b1329763
--- /dev/null
+++ b/frappe/printing/doctype/print_format_field_template/test_print_format_field_template.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+class TestPrintFormatFieldTemplate(unittest.TestCase):
+ pass
diff --git a/frappe/printing/doctype/print_heading/print_heading.py b/frappe/printing/doctype/print_heading/print_heading.py
index f9955c019d..39c46ad152 100644
--- a/frappe/printing/doctype/print_heading/print_heading.py
+++ b/frappe/printing/doctype/print_heading/print_heading.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/printing/doctype/print_heading/test_print_heading.py b/frappe/printing/doctype/print_heading/test_print_heading.py
index ce99cde607..7eaa1bc6ba 100644
--- a/frappe/printing/doctype/print_heading/test_print_heading.py
+++ b/frappe/printing/doctype/print_heading/test_print_heading.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/printing/doctype/print_settings/print_settings.js b/frappe/printing/doctype/print_settings/print_settings.js
index 9616892a31..b1311166ee 100644
--- a/frappe/printing/doctype/print_settings/print_settings.js
+++ b/frappe/printing/doctype/print_settings/print_settings.js
@@ -15,27 +15,5 @@ frappe.ui.form.on('Print Settings', {
},
onload: function(frm) {
frm.script_manager.trigger("print_style");
- },
- server_ip: function(frm) {
- frm.trigger("connect_print_server");
- },
- port:function(frm) {
- frm.trigger("connect_print_server");
- },
- connect_print_server:function(frm) {
- if(frm.doc.server_ip && frm.doc.port){
- frappe.call({
- "doc": frm.doc,
- "method": "get_printers",
- "args": {
- ip: frm.doc.server_ip,
- port: frm.doc.port
- },
- callback: function(data) {
- frm.set_df_property('printer_name', 'options', [""].concat(data.message));
- },
- error: (data) => frm.set_value("enable_print_server", 0)
- });
- }
}
});
diff --git a/frappe/printing/doctype/print_settings/print_settings.json b/frappe/printing/doctype/print_settings/print_settings.json
index d64cb4c6d3..babbae248d 100644
--- a/frappe/printing/doctype/print_settings/print_settings.json
+++ b/frappe/printing/doctype/print_settings/print_settings.json
@@ -19,9 +19,6 @@
"allow_print_for_cancelled",
"server_printer",
"enable_print_server",
- "server_ip",
- "printer_name",
- "port",
"raw_printing_section",
"enable_raw_printing",
"print_style_section",
@@ -107,29 +104,11 @@
},
{
"default": "0",
+ "depends_on": "enable_print_server",
"fieldname": "enable_print_server",
"fieldtype": "Check",
- "label": "Enable Print Server"
- },
- {
- "default": "localhost",
- "depends_on": "enable_print_server",
- "fieldname": "server_ip",
- "fieldtype": "Data",
- "label": "Server IP"
- },
- {
- "depends_on": "enable_print_server",
- "fieldname": "printer_name",
- "fieldtype": "Select",
- "label": "Printer Name"
- },
- {
- "default": "631",
- "depends_on": "enable_print_server",
- "fieldname": "port",
- "fieldtype": "Int",
- "label": "Port"
+ "label": "Enable Print Server",
+ "mandatory_depends_on": "enable_print_server"
},
{
"fieldname": "raw_printing_section",
@@ -148,7 +127,7 @@
"label": "Print Style"
},
{
- "default": "Modern",
+ "default": "Redesign",
"fieldname": "print_style",
"fieldtype": "Link",
"in_list_view": 1,
@@ -183,7 +162,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2020-10-22 23:42:09.471022",
+ "modified": "2021-09-17 12:59:14.783694",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Settings",
diff --git a/frappe/printing/doctype/print_settings/print_settings.py b/frappe/printing/doctype/print_settings/print_settings.py
index 610c083097..ff00317cf8 100644
--- a/frappe/printing/doctype/print_settings/print_settings.py
+++ b/frappe/printing/doctype/print_settings/print_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe import _
@@ -12,26 +12,6 @@ class PrintSettings(Document):
def on_update(self):
frappe.clear_cache()
- @frappe.whitelist()
- def get_printers(self,ip="localhost",port=631):
- printer_list = []
- try:
- import cups
- except ImportError:
- frappe.throw(_("You need to install pycups to use this feature!"))
- return
- try:
- cups.setServer(self.server_ip)
- cups.setPort(self.port)
- conn = cups.Connection()
- printers = conn.getPrinters()
- printer_list = printers.keys()
- except RuntimeError:
- frappe.throw(_("Failed to connect to server"))
- except frappe.ValidationError:
- frappe.throw(_("Failed to connect to server"))
- return printer_list
-
@frappe.whitelist()
def is_print_server_enabled():
if not hasattr(frappe.local, 'enable_print_server'):
diff --git a/frappe/printing/doctype/print_settings/test_print_settings.py b/frappe/printing/doctype/print_settings/test_print_settings.py
index d1dec861b2..82883eaee5 100644
--- a/frappe/printing/doctype/print_settings/test_print_settings.py
+++ b/frappe/printing/doctype/print_settings/test_print_settings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import unittest
class TestPrintSettings(unittest.TestCase):
diff --git a/frappe/printing/doctype/print_style/print_style.py b/frappe/printing/doctype/print_style/print_style.py
index a91786795c..7985c006f4 100644
--- a/frappe/printing/doctype/print_style/print_style.py
+++ b/frappe/printing/doctype/print_style/print_style.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
+# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
diff --git a/frappe/printing/doctype/print_style/test_print_style.py b/frappe/printing/doctype/print_style/test_print_style.py
index b717b23df8..cbf5c465d1 100644
--- a/frappe/printing/doctype/print_style/test_print_style.py
+++ b/frappe/printing/doctype/print_style/test_print_style.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# License: MIT. See LICENSE
import frappe
import unittest
diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js
index 233bbe0ce7..f10c703589 100644
--- a/frappe/printing/page/print/print.js
+++ b/frappe/printing/page/print/print.js
@@ -41,7 +41,11 @@ frappe.ui.form.PrintView = class {
- `
+
+
+
+
+ `
);
this.print_settings = frappe.model.get_doc(
@@ -72,7 +76,7 @@ frappe.ui.form.PrintView = class {
this.page.add_button(
__('PDF'),
- () => this.render_page('/api/method/frappe.utils.print_format.download_pdf?'),
+ () => this.render_pdf(),
{ icon: 'small-file' }
);
@@ -113,22 +117,20 @@ frappe.ui.form.PrintView = class {
},
).$input;
- this.letterhead_selector = this.add_sidebar_item(
+ this.letterhead_selector_df = this.add_sidebar_item(
{
- fieldtype: 'Select',
+ fieldtype: 'Autocomplete',
fieldname: 'letterhead',
label: __('Select Letterhead'),
- options: [
- this.get_default_option_for_select(__('Select Letterhead')),
- __('No Letterhead')
- ],
+ placeholder: __('Select Letterhead'),
+ options: [__('No Letterhead')],
change: () => this.preview(),
default: this.print_settings.with_letterhead
? __('No Letterhead')
: __('Select Letterhead')
},
- ).$input;
-
+ );
+ this.letterhead_selector = this.letterhead_selector_df.$input;
this.sidebar_dynamic_section = $(
``
).appendTo(this.sidebar);
@@ -136,7 +138,7 @@ frappe.ui.form.PrintView = class {
add_sidebar_item(df, is_dynamic) {
if (df.fieldtype == 'Select') {
- df.input_class = 'btn btn-default btn-sm';
+ df.input_class = 'btn btn-default btn-sm text-left';
}
let field = frappe.ui.form.make_control({
@@ -167,20 +169,23 @@ frappe.ui.form.PrintView = class {
frappe.set_route('Form', 'Print Settings');
});
- if (
- frappe.model.get_doc(':Print Settings', 'Print Settings')
- .enable_raw_printing == '1'
- ) {
+ if (this.print_settings.enable_raw_printing == '1') {
this.page.add_menu_item(__('Raw Printing Setting'), () => {
this.printer_setting_dialog();
});
}
- if (frappe.user.has_role('System Manager')) {
+ if (frappe.model.can_create('Print Format')) {
this.page.add_menu_item(__('Customize'), () =>
this.edit_print_format()
);
}
+
+ if (cint(this.print_settings.enable_print_server)) {
+ this.page.add_menu_item(__('Select Network Printer'), () =>
+ this.network_printer_setting_dialog()
+ );
+ }
}
show(frm) {
@@ -189,6 +194,13 @@ frappe.ui.form.PrintView = class {
this.set_breadcrumbs();
this.setup_customize_dialog();
+ // print format builder beta
+ this.page.add_inner_message(`
+
+ ${__('Try the new Print Format Builder')}
+
+ `);
+
let tasks = [
this.refresh_print_options,
this.set_default_print_language,
@@ -232,7 +244,7 @@ frappe.ui.form.PrintView = class {
let print_format = this.get_print_format();
let is_custom_format =
print_format.name &&
- print_format.print_format_builder &&
+ (print_format.print_format_builder || print_format.print_format_builder_beta) &&
print_format.standard === 'No';
let is_standard_but_editable =
print_format.name && print_format.custom_format;
@@ -242,7 +254,11 @@ frappe.ui.form.PrintView = class {
return;
}
if (is_custom_format) {
- frappe.set_route('print-format-builder', print_format.name);
+ if (print_format.print_format_builder_beta) {
+ frappe.set_route('print-format-builder-beta', print_format.name);
+ } else {
+ frappe.set_route('print-format-builder', print_format.name);
+ }
return;
}
// start a new print format
@@ -260,6 +276,11 @@ frappe.ui.form.PrintView = class {
fieldtype: 'Read Only',
default: print_format.name || 'Standard',
},
+ {
+ label: __('Use the new Print Format Builder'),
+ fieldname: 'beta',
+ fieldtype: 'Check'
+ },
],
(data) => {
frappe.route_options = {
@@ -267,6 +288,7 @@ frappe.ui.form.PrintView = class {
doctype: this.frm.doctype,
name: data.print_format_name,
based_on: data.based_on,
+ beta: data.beta
};
frappe.set_route('print-format-builder');
this.print_sel.val(data.print_format_name);
@@ -336,23 +358,19 @@ frappe.ui.form.PrintView = class {
}
set_letterhead_options() {
- let letterhead_options = [
- this.get_default_option_for_select(__('Select Letterhead')),
- __('No Letterhead')
- ];
+ let letterhead_options = [__('No Letterhead')];
let default_letterhead;
let doc_letterhead = this.frm.doc.letter_head;
return frappe.db
- .get_list('Letter Head', { fields: ['name', 'is_default'] })
+ .get_list('Letter Head', { fields: ['name', 'is_default'], limit: 0 })
.then((letterheads) => {
- this.letterhead_selector.empty();
letterheads.map((letterhead) => {
if (letterhead.is_default) default_letterhead = letterhead.name;
return letterhead_options.push(letterhead.name);
});
- this.letterhead_selector.add_options(letterhead_options);
+ this.letterhead_selector_df.set_data(letterhead_options);
let selected_letterhead = doc_letterhead || default_letterhead;
if (selected_letterhead)
this.letterhead_selector.val(selected_letterhead);
@@ -383,6 +401,17 @@ frappe.ui.form.PrintView = class {
}
preview() {
+ let print_format = this.get_print_format();
+ if (print_format.print_format_builder_beta) {
+ this.print_wrapper.find('.print-preview-wrapper').hide();
+ this.print_wrapper.find('.preview-beta-wrapper').show();
+ this.preview_beta();
+ return;
+ }
+
+ this.print_wrapper.find('.preview-beta-wrapper').hide();
+ this.print_wrapper.find('.print-preview-wrapper').show();
+
const $print_format = this.print_wrapper.find('iframe');
this.$print_format_body = $print_format.contents();
this.get_print_html((out) => {
@@ -406,22 +435,32 @@ frappe.ui.form.PrintView = class {
});
}
+ preview_beta() {
+ let print_format = this.get_print_format();
+ const iframe = this.print_wrapper.find('.preview-beta-wrapper iframe');
+ let params = new URLSearchParams({
+ doctype: this.frm.doc.doctype,
+ name: this.frm.doc.name,
+ print_format: print_format.name
+ });
+ let letterhead = this.get_letterhead();
+ if (letterhead) {
+ params.append("letterhead", letterhead);
+ }
+ iframe.prop('src', `/printpreview?${params.toString()}`);
+ }
+
setup_print_format_dom(out, $print_format) {
this.print_wrapper.find('.print-format-skeleton').remove();
let base_url = frappe.urllib.get_base_url();
- let print_css = frappe.assets.bundled_asset('print.bundle.css');
+ let print_css = frappe.assets.bundled_asset('print.bundle.css', frappe.utils.is_rtl(this.lang_code));
+ this.$print_format_body.find('html').attr('dir', frappe.utils.is_rtl(this.lang_code) ? 'rtl': 'ltr');
+ this.$print_format_body.find('html').attr('lang', this.lang_code);
this.$print_format_body.find('head').html(
`
`
);
- if (frappe.utils.is_rtl(this.lang_code)) {
- let rtl_css = frappe.assets.bundled_asset('frappe-rtl.bundle.css');
- this.$print_format_body.find('head').append(
- ``
- );
- }
-
this.$print_format_body.find('body').html(
`${out.html}
`
);
@@ -471,72 +510,128 @@ frappe.ui.form.PrintView = class {
printit() {
let me = this;
- frappe.call({
- method:
- 'frappe.printing.doctype.print_settings.print_settings.is_print_server_enabled',
- callback: function(data) {
- if (data.message) {
- frappe.call({
- method: 'frappe.utils.print_format.print_by_server',
- args: {
- doctype: me.frm.doc.doctype,
- name: me.frm.doc.name,
- print_format: me.selected_format(),
- no_letterhead: me.with_letterhead(),
- letterhead: this.get_letterhead(),
- },
- callback: function() {},
- });
- } else if (me.get_mapped_printer().length === 1) {
- // printer is already mapped in localstorage (applies for both raw and pdf )
- if (me.is_raw_printing()) {
- me.get_raw_commands(function(out) {
- frappe.ui.form
- .qz_connect()
- .then(function() {
- let printer_map = me.get_mapped_printer()[0];
- let data = [out.raw_commands];
- let config = qz.configs.create(printer_map.printer);
- return qz.print(config, data);
- })
- .then(frappe.ui.form.qz_success)
- .catch((err) => {
- frappe.ui.form.qz_fail(err);
- });
+
+ if (cint(me.print_settings.enable_print_server)) {
+ if (localStorage.getItem('network_printer')) {
+ me.print_by_server();
+ } else {
+ me.network_printer_setting_dialog(() => me.print_by_server());
+ }
+ } else if (me.get_mapped_printer().length === 1) {
+ // printer is already mapped in localstorage (applies for both raw and pdf )
+ if (me.is_raw_printing()) {
+ me.get_raw_commands(function(out) {
+ frappe.ui.form
+ .qz_connect()
+ .then(function() {
+ let printer_map = me.get_mapped_printer()[0];
+ let data = [out.raw_commands];
+ let config = qz.configs.create(printer_map.printer);
+ return qz.print(config, data);
+ })
+ .then(frappe.ui.form.qz_success)
+ .catch((err) => {
+ frappe.ui.form.qz_fail(err);
});
- } else {
- frappe.show_alert(
+ });
+ } else {
+ frappe.show_alert(
+ {
+ message: __('PDF printing via "Raw Print" is not supported.'),
+ subtitle: __(
+ 'Please remove the printer mapping in Printer Settings and try again.'
+ ),
+ indicator: 'info',
+ },
+ 14
+ );
+ //Note: need to solve "Error: Cannot parse (FILE) as a PDF file" to enable qz pdf printing.
+ }
+ } else if (me.is_raw_printing()) {
+ // printer not mapped in localstorage and the current print format is raw printing
+ frappe.show_alert(
+ {
+ message: __('Printer mapping not set.'),
+ subtitle: __(
+ 'Please set a printer mapping for this print format in the Printer Settings'
+ ),
+ indicator: 'warning',
+ },
+ 14
+ );
+ me.printer_setting_dialog();
+ } else {
+ me.render_page('/printview?', true);
+ }
+ }
+
+ print_by_server() {
+ let me = this;
+ if (localStorage.getItem('network_printer')) {
+ frappe.call({
+ method: 'frappe.utils.print_format.print_by_server',
+ args: {
+ doctype: me.frm.doc.doctype,
+ name: me.frm.doc.name,
+ printer_setting: localStorage.getItem('network_printer'),
+ print_format: me.selected_format(),
+ no_letterhead: me.with_letterhead(),
+ letterhead: me.get_letterhead(),
+ },
+ callback: function() {},
+ });
+ }
+ }
+ network_printer_setting_dialog(callback) {
+ frappe.call({
+ method: 'frappe.printing.doctype.network_printer_settings.network_printer_settings.get_network_printer_settings',
+ callback: function(r) {
+ if (r.message) {
+ let d = new frappe.ui.Dialog({
+ title: __('Select Network Printer'),
+ fields: [
{
- message: __('PDF printing via "Raw Print" is not supported.'),
- subtitle: __(
- 'Please remove the printer mapping in Printer Settings and try again.'
- ),
- indicator: 'info',
- },
- 14
- );
- //Note: need to solve "Error: Cannot parse (FILE) as a PDF file" to enable qz pdf printing.
- }
- } else if (me.is_raw_printing()) {
- // printer not mapped in localstorage and the current print format is raw printing
- frappe.show_alert(
- {
- message: __('Printer mapping not set.'),
- subtitle: __(
- 'Please set a printer mapping for this print format in the Printer Settings'
- ),
- indicator: 'warning',
+ "label": "Printer",
+ "fieldname": "printer",
+ "fieldtype": "Select",
+ "reqd": 1,
+ "options": r.message
+ }
+ ],
+ primary_action: function() {
+ localStorage.setItem('network_printer', d.get_values().printer);
+ if (typeof callback == "function") {
+ callback();
+ }
+ d.hide();
},
- 14
- );
- me.printer_setting_dialog();
- } else {
- me.render_page('/printview?', true);
+ primary_action_label: __('Select')
+ });
+ d.show();
}
},
});
}
+ render_pdf() {
+ let print_format = this.get_print_format();
+ if (print_format.print_format_builder_beta) {
+ let params = new URLSearchParams({
+ doctype: this.frm.doc.doctype,
+ name: this.frm.doc.name,
+ print_format: print_format.name,
+ letterhead: this.get_letterhead()
+ });
+ let w = window.open(`/api/method/frappe.utils.weasyprint.download_pdf?${params}`);
+ if (!w) {
+ frappe.msgprint(__('Please enable pop-ups'));
+ return;
+ }
+ } else {
+ this.render_page('/api/method/frappe.utils.print_format.download_pdf?');
+ }
+ }
+
render_page(method, printit = false) {
let w = window.open(
frappe.urllib.get_full_url(
diff --git a/frappe/printing/page/print_format_builder/print_format_builder.js b/frappe/printing/page/print_format_builder/print_format_builder.js
index ca2a8bc378..313e8da539 100644
--- a/frappe/printing/page/print_format_builder/print_format_builder.js
+++ b/frappe/printing/page/print_format_builder/print_format_builder.js
@@ -12,9 +12,9 @@ frappe.pages['print-format-builder'].on_page_show = function(wrapper) {
});
} else if(frappe.route_options) {
if(frappe.route_options.make_new) {
- let { doctype, name, based_on } = frappe.route_options;
+ let { doctype, name, based_on, beta } = frappe.route_options;
frappe.route_options = null;
- frappe.print_format_builder.setup_new_print_format(doctype, name, based_on);
+ frappe.print_format_builder.setup_new_print_format(doctype, name, based_on, beta);
} else {
frappe.print_format_builder.print_format = frappe.route_options.doc;
frappe.route_options = null;
@@ -126,18 +126,22 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
});
}
- setup_new_print_format(doctype, name, based_on) {
+ setup_new_print_format(doctype, name, based_on, beta) {
frappe.call({
method: 'frappe.printing.page.print_format_builder.print_format_builder.create_custom_format',
args: {
doctype: doctype,
name: name,
- based_on: based_on
+ based_on: based_on,
+ beta: Boolean(beta)
},
callback: (r) => {
- if(!r.exc) {
- if(r.message) {
- this.print_format = r.message;
+ if (r.message) {
+ let print_format = r.message;
+ if (print_format.print_format_builder_beta) {
+ frappe.set_route('print-format-builder-beta', print_format.name);
+ } else {
+ this.print_format = print_format;
this.refresh();
}
}
@@ -261,7 +265,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
} else if(f.fieldtype==="Column Break") {
set_column();
- } else if(!in_list(["Section Break", "Column Break", "Fold"], f.fieldtype)
+ } else if (!in_list(["Section Break", "Column Break", "Tab Break", "Fold"], f.fieldtype)
&& f.label) {
if(!column) set_column();
@@ -298,7 +302,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
init_visible_columns(f) {
f.visible_columns = []
$.each(frappe.get_meta(f.options).fields, function(i, _f) {
- if(!in_list(["Section Break", "Column Break"], _f.fieldtype) &&
+ if (!in_list(["Section Break", "Column Break", "Tab Break"], _f.fieldtype) &&
!_f.print_hide && f.label) {
// column names set as fieldname|width
@@ -606,7 +610,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
// add remaining fields
$.each(doc_fields, function(j, f) {
if (f && !in_list(column_names, f.fieldname)
- && !in_list(["Section Break", "Column Break"], f.fieldtype) && f.label) {
+ && !in_list(["Section Break", "Column Break", "Tab Break"], f.fieldtype) && f.label) {
fields.push(f);
}
})
diff --git a/frappe/printing/page/print_format_builder/print_format_builder.py b/frappe/printing/page/print_format_builder/print_format_builder.py
index d9f57762b0..fae564d3c3 100644
--- a/frappe/printing/page/print_format_builder/print_format_builder.py
+++ b/frappe/printing/page/print_format_builder/print_format_builder.py
@@ -1,11 +1,16 @@
import frappe
@frappe.whitelist()
-def create_custom_format(doctype, name, based_on='Standard'):
+def create_custom_format(doctype, name, based_on='Standard', beta=False):
doc = frappe.new_doc('Print Format')
doc.doc_type = doctype
doc.name = name
- doc.print_format_builder = 1
+ beta = frappe.parse_json(beta)
+
+ if beta:
+ doc.print_format_builder_beta = 1
+ else:
+ doc.print_format_builder = 1
doc.format_data = frappe.db.get_value('Print Format', based_on, 'format_data') \
if based_on != 'Standard' else None
doc.insert()
diff --git a/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html b/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html
index 1ebb87ac31..c608eecbbd 100644
--- a/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html
+++ b/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html
@@ -4,7 +4,7 @@
+
{
+ if (!resp.exc) {
+ this.google_drive_settings = resp.message;
+ }
+ }
+ });
}
},
watch: {
@@ -187,9 +231,6 @@ export default {
return this.files.length > 0
&& this.files.every(
file => file.total !== 0 && file.progress === file.total);
- },
- allow_take_photo() {
- return window.navigator.mediaDevices;
}
},
methods: {
@@ -212,6 +253,11 @@ export default {
remove_file(file) {
this.files = this.files.filter(f => f !== file);
},
+ toggle_image_cropper(index) {
+ this.crop_image_with_index = this.show_image_cropper ? -1 : index;
+ this.hide_dialog_footer = !this.show_image_cropper;
+ this.show_image_cropper = !this.show_image_cropper;
+ },
toggle_all_private() {
let flag;
let private_values = this.files.filter(file => file.private);
@@ -235,6 +281,9 @@ export default {
let is_image = file.type.startsWith('image');
return {
file_obj: file,
+ cropper_file: file,
+ crop_box_data: null,
+ optimize: this.attach_doc_image ? true : false,
name: file.name,
doc: null,
progress: 0,
@@ -245,6 +294,9 @@ export default {
}
});
this.files = this.files.concat(files);
+ if(this.files.length != 0 && this.attach_doc_image) {
+ this.toggle_image_cropper(0);
+ }
},
check_restrictions(file) {
let { max_file_size, allowed_file_types } = this.restrictions;
@@ -408,6 +460,10 @@ export default {
form_data.append('file_url', file.file_url);
}
+ if (file.file_name) {
+ form_data.append('file_name', file.file_name);
+ }
+
if (this.doctype && this.docname) {
form_data.append('doctype', this.doctype);
form_data.append('docname', this.docname);
@@ -421,6 +477,15 @@ export default {
form_data.append('method', this.method);
}
+ if (file.optimize) {
+ form_data.append('optimize', true);
+ }
+
+ if (this.attach_doc_image) {
+ form_data.append('max_width', 200);
+ form_data.append('max_height', 200);
+ }
+
xhr.send(form_data);
});
},
@@ -437,6 +502,25 @@ export default {
);
});
},
+ show_google_drive_picker() {
+ let dialog = cur_dialog;
+ dialog.hide();
+ let google_drive = new GoogleDrivePicker({
+ pickerCallback: data => this.google_drive_callback(data, dialog),
+ ...this.google_drive_settings
+ });
+ google_drive.loadPicker();
+ },
+ google_drive_callback(data, dialog) {
+ if (data.action == google.picker.Action.PICKED) {
+ this.upload_file({
+ file_url: data.docs[0].url,
+ file_name: data.docs[0].name
+ });
+ } else if (data.action == google.picker.Action.CANCEL) {
+ dialog.show();
+ }
+ },
url_to_file(url, filename, mime_type) {
return fetch(url)
.then(res => res.arrayBuffer())
diff --git a/frappe/public/js/frappe/file_uploader/ImageCropper.vue b/frappe/public/js/frappe/file_uploader/ImageCropper.vue
new file mode 100644
index 0000000000..09b50390fe
--- /dev/null
+++ b/frappe/public/js/frappe/file_uploader/ImageCropper.vue
@@ -0,0 +1,80 @@
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frappe/public/js/frappe/file_uploader/index.js b/frappe/public/js/frappe/file_uploader/index.js
index 28ce96cd44..87bc1c8ec8 100644
--- a/frappe/public/js/frappe/file_uploader/index.js
+++ b/frappe/public/js/frappe/file_uploader/index.js
@@ -15,6 +15,7 @@ export default class FileUploader {
allow_multiple,
as_dataurl,
disable_file_browser,
+ attach_doc_image,
frm
} = {}) {
@@ -26,6 +27,10 @@ export default class FileUploader {
this.wrapper = wrapper.get ? wrapper.get(0) : wrapper;
}
+ if (attach_doc_image) {
+ restrictions.allowed_file_types = ['image/jpeg', 'image/png'];
+ }
+
this.$fileuploader = new Vue({
el: this.wrapper,
render: h => h(FileUploaderComponent, {
@@ -42,6 +47,7 @@ export default class FileUploader {
allow_multiple,
as_dataurl,
disable_file_browser,
+ attach_doc_image,
}
})
});
@@ -55,6 +61,22 @@ export default class FileUploader {
}
}, { deep: true });
+ this.uploader.$watch('trigger_upload', (trigger_upload) => {
+ if (trigger_upload) {
+ this.upload_files();
+ }
+ });
+
+ this.uploader.$watch('hide_dialog_footer', (hide_dialog_footer) => {
+ if (hide_dialog_footer) {
+ this.dialog && this.dialog.footer.addClass('hide');
+ this.dialog.$wrapper.data('bs.modal')._config.backdrop = 'static';
+ } else {
+ this.dialog && this.dialog.footer.removeClass('hide');
+ this.dialog.$wrapper.data('bs.modal')._config.backdrop = true;
+ }
+ });
+
if (files && files.length) {
this.uploader.add_files(files);
}
diff --git a/frappe/public/js/frappe/form/column.js b/frappe/public/js/frappe/form/column.js
new file mode 100644
index 0000000000..27231da7e5
--- /dev/null
+++ b/frappe/public/js/frappe/form/column.js
@@ -0,0 +1,49 @@
+export default class Column {
+ constructor(section, df) {
+ if (!df) df = {};
+
+ this.df = df;
+ this.section = section;
+ this.make();
+ this.resize_all_columns();
+ }
+
+ make() {
+ this.wrapper = $(`
+
+
+
+ `)
+ .appendTo(this.section.body)
+ .find("form")
+ .on("submit", function () {
+ return false;
+ });
+
+ if (this.df.label) {
+ $(`
+
+ `)
+ .appendTo(this.wrapper);
+ }
+ }
+
+ resize_all_columns() {
+ // distribute all columns equally
+ let colspan = cint(12 / this.section.wrapper.find(".form-column").length);
+
+ this.section.wrapper
+ .find(".form-column")
+ .removeClass()
+ .addClass("form-column")
+ .addClass("col-sm-" + colspan);
+
+ }
+
+ refresh() {
+ this.section.refresh();
+ }
+}
\ No newline at end of file
diff --git a/frappe/public/js/frappe/form/controls/attach.js b/frappe/public/js/frappe/form/controls/attach.js
index 672087ddc2..bd66225171 100644
--- a/frappe/public/js/frappe/form/controls/attach.js
+++ b/frappe/public/js/frappe/form/controls/attach.js
@@ -4,8 +4,13 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro
this.$input = $('