diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index 93189d2b1f..6c81d6298a 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -59,4 +59,4 @@ cd ../..
bench start &
bench --site test_site reinstall --yes
if [ "$TYPE" == "server" ]; then bench --site test_site_producer reinstall --yes; fi
-bench build --app frappe
+CI=Yes bench build --app frappe
diff --git a/.github/workflows/patch-mariadb-tests.yml b/.github/workflows/patch-mariadb-tests.yml
index 56137d0bea..8758c4e273 100644
--- a/.github/workflows/patch-mariadb-tests.yml
+++ b/.github/workflows/patch-mariadb-tests.yml
@@ -29,7 +29,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v2
with:
- python-version: 3.7
+ python-version: '3.9'
- name: Check if build should be run
id: check-build
diff --git a/.github/workflows/publish-assets-develop.yml b/.github/workflows/publish-assets-develop.yml
index 85f3f7c3b0..f56d1460b5 100644
--- a/.github/workflows/publish-assets-develop.yml
+++ b/.github/workflows/publish-assets-develop.yml
@@ -18,7 +18,7 @@ jobs:
node-version: 14
- uses: actions/setup-python@v2
with:
- python-version: '3.7'
+ python-version: '3.9'
- name: Set up bench and build assets
run: |
npm install -g yarn
diff --git a/.github/workflows/publish-assets-releases.yml b/.github/workflows/publish-assets-releases.yml
index a5cc1f8872..2582632fa0 100644
--- a/.github/workflows/publish-assets-releases.yml
+++ b/.github/workflows/publish-assets-releases.yml
@@ -21,7 +21,7 @@ jobs:
python-version: '12.x'
- uses: actions/setup-python@v2
with:
- python-version: '3.7'
+ python-version: '3.9'
- name: Set up bench and build assets
run: |
npm install -g yarn
diff --git a/.github/workflows/server-mariadb-tests.yml b/.github/workflows/server-mariadb-tests.yml
index 2e50aa48f6..588f357f26 100644
--- a/.github/workflows/server-mariadb-tests.yml
+++ b/.github/workflows/server-mariadb-tests.yml
@@ -38,7 +38,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v2
with:
- python-version: 3.7
+ python-version: '3.9'
- name: Check if build should be run
id: check-build
diff --git a/.github/workflows/server-postgres-tests.yml b/.github/workflows/server-postgres-tests.yml
index 2203b657ad..78f379837b 100644
--- a/.github/workflows/server-postgres-tests.yml
+++ b/.github/workflows/server-postgres-tests.yml
@@ -41,7 +41,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v2
with:
- python-version: 3.7
+ python-version: '3.9'
- name: Check if build should be run
id: check-build
diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
index a9d331c44d..fcc53ba59c 100644
--- a/.github/workflows/ui-tests.yml
+++ b/.github/workflows/ui-tests.yml
@@ -37,7 +37,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v2
with:
- python-version: 3.7
+ python-version: '3.9'
- name: Check if build should be run
id: check-build
diff --git a/README.md b/README.md
index 6c2804d843..f8a1907da2 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@
-
+
@@ -35,25 +35,29 @@
Full-stack web application framework that uses Python and MariaDB on the server side and a tightly integrated client side library. Built for [ERPNext](https://erpnext.com)
-### Table of Contents
-* [Installation](https://frappeframework.com/docs/user/en/installation)
-* [Documentation](https://frappeframework.com/docs)
+## Table of Contents
+* [Installation](#installation)
+* [Contributing](#contributing)
+* [Resources](#resources)
* [License](#license)
-### Installation
+## Installation
* [Install via Docker](https://github.com/frappe/frappe_docker)
* [Install via Frappe Bench](https://github.com/frappe/bench)
+* [Offical Documentation](https://frappeframework.com/docs/user/en/installation)
## Contributing
+1. [Code of Conduct](CODE_OF_CONDUCT.md)
1. [Contribution Guidelines](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
+1. [Security Policy](SECURITY.md)
1. [Translations](https://translate.erpnext.com)
-### Website
+## Resources
-For details and documentation, see the website
-[https://frappeframework.com](https://frappeframework.com)
+1. [frappeframework.com](https://frappeframework.com) - Official documentation of the Frappe Framework.
+1. [frappe.school](https://frappe.school) - Pick from the various courses by the maintainers or from the community.
-### License
+## License
This repository has been released under the [MIT License](LICENSE).
diff --git a/cypress/integration/discussions.js b/cypress/integration/discussions.js
index 3c055d1923..a6e0ff9b56 100644
--- a/cypress/integration/discussions.js
+++ b/cypress/integration/discussions.js
@@ -57,7 +57,23 @@ context('Discussions', () => {
cy.get('.discussion-on-page:visible .comment-field').should('have.value', '');
};
+ const single_thread_discussion = () => {
+ cy.visit('/test-single-thread');
+ cy.get('.discussions-sidebar').should('have.length', 0);
+ cy.get('.reply').should('have.length', 0);
+
+ cy.get('.discussion-on-page .comment-field')
+ .type('This comment is being made on a single thread discussion.')
+ .should('have.value', 'This comment is being made on a single thread discussion.');
+
+ cy.get('.discussion-on-page .submit-discussion').click();
+ cy.wait(3000);
+ cy.get('.discussion-on-page').children(".reply-card").eq(-1).children(".reply-text")
+ .should('have.text', 'This comment is being made on a single thread discussion.\n');
+ };
+
it('reply through modal', reply_through_modal);
it('reply through comment box', reply_through_comment_box);
it('cancel and clear comment box', cancel_and_clear_comment_box);
+ it('single thread discussion', single_thread_discussion);
});
diff --git a/cypress/integration/timeline.js b/cypress/integration/timeline.js
index 66ba0761c4..3071330b61 100644
--- a/cypress/integration/timeline.js
+++ b/cypress/integration/timeline.js
@@ -44,13 +44,14 @@ context('Timeline', () => {
cy.get('.timeline-content').should('contain', 'Testing Timeline 123');
//Deleting the added comment
- cy.get('.actions > .btn > .icon').first().click();
+ cy.get('.more-actions > .action-btn').click();
+ cy.get('.more-actions .dropdown-item').contains('Delete').click();
cy.findByRole('button', {name: 'Yes'}).click();
cy.click_modal_primary_button('Yes');
//Deleting the added ToDo
- cy.get('.menu-btn-group button').eq(1).click();
- cy.get('.menu-btn-group [data-label="Delete"]').click();
+ cy.get('.menu-btn-group [data-original-title="Menu"]').click();
+ cy.get('.menu-btn-group .dropdown-item').contains('Delete').click();
cy.findByRole('button', {name: 'Yes'}).click();
});
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 47c37a56a0..6484370946 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -353,5 +353,5 @@ Cypress.Commands.add('click_listview_primary_button', (btn_name) => {
});
Cypress.Commands.add('click_timeline_action_btn', (btn_name) => {
- cy.get('.timeline-content > .timeline-message-box > .justify-between > .actions > .btn').contains(btn_name).click();
+ cy.get('.timeline-message-box .custom-actions > .btn').contains(btn_name).click();
});
\ No newline at end of file
diff --git a/esbuild/esbuild.js b/esbuild/esbuild.js
index 9074beae06..bf4436358e 100644
--- a/esbuild/esbuild.js
+++ b/esbuild/esbuild.js
@@ -104,6 +104,9 @@ async function execute() {
log_error("There were some problems during build");
log();
log(chalk.dim(e.stack));
+ if (process.env.CI) {
+ process.kill(process.pid);
+ }
return;
}
@@ -528,4 +531,4 @@ function log_rebuilt_assets(prev_assets, new_assets) {
log(" " + filename);
}
log();
-}
\ No newline at end of file
+}
diff --git a/frappe/build.py b/frappe/build.py
index 879d5ec432..05fa213018 100644
--- a/frappe/build.py
+++ b/frappe/build.py
@@ -246,7 +246,7 @@ def bundle(mode, apps=None, hard_link=False, make_copy=False, restore=False, ver
check_node_executable()
frappe_app_path = frappe.get_app_path("frappe", "..")
- frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env())
+ frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env(), raise_err=True)
def watch(apps=None):
diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json
index 18435f8873..c85b4e8f67 100644
--- a/frappe/core/doctype/doctype/doctype.json
+++ b/frappe/core/doctype/doctype/doctype.json
@@ -1,680 +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",
- "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"
- ],
- "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": "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"
- }
- ],
- "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-09-05 15: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
-}
\ No newline at end of file
+ "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": "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": "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-09-05 15: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/feedback/test_feedback.py b/frappe/core/doctype/feedback/test_feedback.py
index 7a722ae0d1..f3cf8dfe6b 100644
--- a/frappe/core/doctype/feedback/test_feedback.py
+++ b/frappe/core/doctype/feedback/test_feedback.py
@@ -5,6 +5,13 @@ 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()
@@ -12,7 +19,14 @@ class TestFeedback(unittest.TestCase):
frappe.db.delete("Feedback", {"reference_doctype": "Blog Post"})
from frappe.templates.includes.feedback.feedback import add_feedback, update_feedback
- feedback = add_feedback('Blog Post', test_blog.name, 5, 'New 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)
diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py
index d9ecd85533..4df9ef3132 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -813,7 +813,7 @@ 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):
@@ -846,7 +846,8 @@ def extract_images_from_html(doc, content):
"attached_to_doctype": doctype,
"attached_to_name": name,
"content": content,
- "decode": False
+ "decode": False,
+ "is_private": is_private
})
_file.save(ignore_permissions=True)
file_url = _file.file_url
diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py
index 79fe7a9140..5b1aab1241 100644
--- a/frappe/core/doctype/server_script/server_script.py
+++ b/frappe/core/doctype/server_script/server_script.py
@@ -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 6c028ff136..de858327a9 100644
--- a/frappe/core/doctype/server_script/test_server_script.py
+++ b/frappe/core/doctype/server_script/test_server_script.py
@@ -59,6 +59,26 @@ 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()
+'''
+ ),
+ dict(
+ name='test_cache_methods',
+ script_type = 'DocType Event',
+ doctype_event = 'Before Save',
+ reference_doctype = 'ToDo',
+ disabled = 1,
+ script = '''
+frappe.cache().set_value('test_key', doc.name)
'''
)
]
@@ -119,3 +139,24 @@ class TestServerScript(unittest.TestCase):
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()
+
+ def test_cache_methods_in_server_script(self):
+ server_script = frappe.get_doc('Server Script', 'test_cache_methods')
+ server_script.disabled = 0
+ server_script.save()
+
+ todo = frappe.get_doc(dict(doctype='ToDo', description='test me')).insert()
+ self.assertEqual(todo.name, frappe.cache().get_value('test_key'))
+
+ server_script.disabled = 1
+ server_script.save()
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/database/database.py b/frappe/database/database.py
index f07e0c38e3..e98cc22f41 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -113,6 +113,7 @@ class Database(object):
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)
diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql
index 670fb71aa2..73b98f0ff3 100644
--- a/frappe/database/mariadb/framework_mariadb.sql
+++ b/frappe/database/mariadb/framework_mariadb.sql
@@ -226,6 +226,7 @@ CREATE TABLE `tabDocType` (
`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=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql
index 868f98fc98..e8e047f194 100644
--- a/frappe/database/postgres/framework_postgres.sql
+++ b/frappe/database/postgres/framework_postgres.sql
@@ -231,6 +231,7 @@ CREATE TABLE "tabDocType" (
"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/desk/form/utils.py b/frappe/desk/form/utils.py
index a4dcee4ab3..1c954edff0 100644
--- a/frappe/desk/form/utils.py
+++ b/frappe/desk/form/utils.py
@@ -66,7 +66,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/reportview.py b/frappe/desk/reportview.py
index c75d730b2f..31eb224652 100644
--- a/frappe/desk/reportview.py
+++ b/frappe/desk/reportview.py
@@ -127,6 +127,8 @@ def setup_group_by(data):
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)
diff --git a/frappe/email/doctype/notification/test_notification.py b/frappe/email/doctype/notification/test_notification.py
index a086ded3fb..f05d35be3e 100644
--- a/frappe/email/doctype/notification/test_notification.py
+++ b/frappe/email/doctype/notification/test_notification.py
@@ -274,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/installer.py b/frappe/installer.py
index 8b840ede46..1fe891c852 100755
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -4,6 +4,8 @@
import json
import os
import sys
+from collections import OrderedDict
+from typing import List, Dict
import frappe
from frappe.defaults import _clear_cache
@@ -158,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)
@@ -230,9 +232,29 @@ 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")
+
+ 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)
+ frappe.db.commit()
+
+ click.secho(f"Uninstalled App {app_name} from Site {site}", fg="green")
+ 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}'")
@@ -242,45 +264,67 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
print(f"* removing DocType '{doctype.name}'...")
if not dry_run:
- frappe.delete_doc("DocType", doctype.name, ignore_on_trash=True)
-
- if not doctype.issingle:
+ if doctype.issingle:
+ frappe.delete_doc("DocType", doctype.name, ignore_on_trash=True)
+ else:
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)
+ _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)
- for doctype in set(drop_doctypes):
+ 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}`")
- if not dry_run:
- remove_from_installed_apps(app_name)
- frappe.db.commit()
-
- click.secho(f"Uninstalled App {app_name} from Site {site}", fg="green")
- frappe.flags.in_uninstall = False
-
def post_install(rebuild_website=False):
from frappe.website.utils import clear_website_cache
@@ -456,9 +500,21 @@ 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
- contents = open(sql_file_path).read()
- with open(sql_file_path, "w") as f:
- f.write(contents.replace("ROW_FORMAT=COMPRESSED", "ROW_FORMAT=DYNAMIC"))
+ 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.unlink(missing_ok=True)
+ 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(missing_ok=True)
def extract_sql_gzip(sql_gz_path):
diff --git a/frappe/migrate.py b/frappe/migrate.py
index 92258502e4..6abc38796f 100644
--- a/frappe/migrate.py
+++ b/frappe/migrate.py
@@ -18,6 +18,7 @@ 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()
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index 5605ac61ed..1826cca9a3 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -267,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:
diff --git a/frappe/model/sync.py b/frappe/model/sync.py
index 138f9eaad4..42bb16cbc2 100644
--- a/frappe/model/sync.py
+++ b/frappe/model/sync.py
@@ -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,17 +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',
- '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']
+ 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):
@@ -95,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/modules/import_file.py b/frappe/modules/import_file.py
index e7a1f5f97c..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
# License: MIT. See LICENSE
-import frappe, os, json
-from frappe.modules import get_module_path, scrub_dt_dn
-from frappe.utils import get_datetime_str
+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,77 +55,160 @@ 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 and not is_changed(doc):
- return False
- original_modified = doc.get("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)
- import_doc(doc, force=force, data_import=data_import, pre_process=pre_process,
- ignore_version=ignore_version, reset_permissions=reset_permissions, path=path)
+ 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 original_modified:
- update_modified(original_modified, doc)
+ # if hash exists and is equal no need to update
+ if stored_hash and stored_hash == calculated_hash:
+ return False
+
+ # 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
+
+ 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 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_changed(doc):
+
+def is_timestamp_changed(doc):
# 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):
- return False
- return True
+ 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 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":
- 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"] == doc["name"] and doc["name"] != "DocType":
+ singles_table = DocType("Singles")
-def import_doc(docdict, force=False, data_import=False, pre_process=None,
- ignore_version=None, reset_permissions=False, path=None):
+ 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)
@@ -132,15 +237,16 @@ def import_doc(docdict, force=False, data_import=False, pre_process=None,
return doc
+
def load_code_properties(doc, path):
- '''Load code files stored in separate files with extensions'''
+ """Load code files stored in separate files with extensions"""
if path:
- if hasattr(doc, 'get_code_fields'):
+ 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)
+ codefile = os.path.join(dirname, filename.split(".")[0] + "." + extn)
if os.path.exists(codefile):
- with open(codefile,'r') as txtfile:
+ with open(codefile, "r") as txtfile:
doc.set(key, txtfile.read())
@@ -164,12 +270,13 @@ def delete_old_doc(doc, reset_permissions):
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]):
+ 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/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/printing/doctype/network_printer_settings/network_printer_settings.json b/frappe/printing/doctype/network_printer_settings/network_printer_settings.json
index cbef4b8ba4..11f1382225 100644
--- a/frappe/printing/doctype/network_printer_settings/network_printer_settings.json
+++ b/frappe/printing/doctype/network_printer_settings/network_printer_settings.json
@@ -41,10 +41,11 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-09-17 11:30:16.781655",
+ "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": [
{
@@ -58,6 +59,15 @@
"role": "System Manager",
"share": 1,
"write": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "All",
+ "share": 1
}
],
"sort_field": "modified",
diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js
index adc5e2363c..7b7009dbaf 100644
--- a/frappe/printing/doctype/print_format/print_format.js
+++ b/frappe/printing/doctype/print_format/print_format.js
@@ -36,7 +36,7 @@ frappe.ui.form.on("Print Format", {
else if (frm.doc.custom_format && !frm.doc.raw_printing) {
frm.set_df_property("html", "reqd", 1);
}
- if (frappe.perm.has_perm('DocType', 0, 'read', frm.doc.doc_type)) {
+ 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 () {
diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js
index 12a48eaa88..1e158c616e 100644
--- a/frappe/printing/page/print/print.js
+++ b/frappe/printing/page/print/print.js
@@ -171,13 +171,13 @@ frappe.ui.form.PrintView = class {
});
}
- if (frappe.perm.has_perm('Print Format', 0, 'create')) {
+ if (frappe.model.can_create('Print Format')) {
this.page.add_menu_item(__('Customize'), () =>
this.edit_print_format()
);
}
- if (this.print_settings.enable_print_server) {
+ if (cint(this.print_settings.enable_print_server)) {
this.page.add_menu_item(__('Select Network Printer'), () =>
this.network_printer_setting_dialog()
);
diff --git a/frappe/public/js/frappe/form/footer/base_timeline.js b/frappe/public/js/frappe/form/footer/base_timeline.js
index 702d964442..beeba16459 100644
--- a/frappe/public/js/frappe/form/footer/base_timeline.js
+++ b/frappe/public/js/frappe/form/footer/base_timeline.js
@@ -97,9 +97,13 @@ class BaseTimeline {
}
timeline_item.append(``);
- timeline_item.find('.timeline-content').append(item.content);
+ let timeline_content = timeline_item.find('.timeline-content');
+ timeline_content.append(item.content);
if (!item.hide_timestamp && !item.is_card) {
- timeline_item.find('.timeline-content').append(`
- ${comment_when(item.creation)}`);
+ timeline_content.append(`
- ${comment_when(item.creation)}`);
+ }
+ if (item.id) {
+ timeline_content.attr("id", item.id);
}
return timeline_item;
}
diff --git a/frappe/public/js/frappe/form/footer/form_timeline.js b/frappe/public/js/frappe/form/footer/form_timeline.js
index b3feae3ee8..128bd355ad 100644
--- a/frappe/public/js/frappe/form/footer/form_timeline.js
+++ b/frappe/public/js/frappe/form/footer/form_timeline.js
@@ -96,6 +96,7 @@ class FormTimeline extends BaseTimeline {
render_timeline_items() {
super.render_timeline_items();
this.set_document_info();
+ frappe.utils.bind_actions_with_object(this.timeline_items_wrapper, this);
}
set_document_info() {
@@ -179,6 +180,7 @@ class FormTimeline extends BaseTimeline {
is_card: true,
content: this.get_communication_timeline_content(communication),
doctype: "Communication",
+ id: `communication-${communication.name}`,
name: communication.name
});
});
@@ -246,6 +248,7 @@ class FormTimeline extends BaseTimeline {
creation: comment.creation,
is_card: true,
doctype: "Comment",
+ id: `comment-${comment.name}`,
name: comment.name,
content: this.get_comment_timeline_content(comment),
};
@@ -394,7 +397,7 @@ class FormTimeline extends BaseTimeline {
}
setup_reply(communication_box, communication_doc) {
- let actions = communication_box.find('.actions');
+ let actions = communication_box.find('.custom-actions');
let reply = $(`
${frappe.utils.icon('reply', 'md')}`).click(() => {
this.compose_mail(communication_doc);
});
@@ -446,14 +449,16 @@ class FormTimeline extends BaseTimeline {
let edit_wrapper = $(`
+-
+ {{ __('Copy Link') }}
+
+
+