diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index 6c81d6298a..454cc89694 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -50,6 +50,7 @@ if [ "$TYPE" == "server" ]; then sed -i 's/^socketio:/# socketio:/g' Procfile; f
if [ "$TYPE" == "server" ]; then sed -i 's/^redis_socketio:/# redis_socketio:/g' Procfile; fi
if [ "$TYPE" == "ui" ]; then bench setup requirements --node; fi
+if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
# install node-sass which is required for website theme test
cd ./apps/frappe || exit
diff --git a/.github/helper/semgrep_rules/README.md b/.github/helper/semgrep_rules/README.md
deleted file mode 100644
index 670d8d280f..0000000000
--- a/.github/helper/semgrep_rules/README.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Semgrep linting
-
-## What is semgrep?
-Semgrep or "semantic grep" is language agnostic static analysis tool. In simple terms semgrep is syntax-aware `grep`, so unlike regex it doesn't get confused by different ways of writing same thing or whitespaces or code split in multiple lines etc.
-
-Example:
-
-To check if a translate function is using f-string or not the regex would be `r"_\(\s*f[\"']"` while equivalent rule in semgrep would be `_(f"...")`. As semgrep knows grammer of language it takes care of unnecessary whitespace, type of quotation marks etc.
-
-You can read more such examples in `.github/helper/semgrep_rules` directory.
-
-# Why/when to use this?
-We want to maintain quality of contributions, at the same time remembering all the good practices can be pain to deal with while evaluating contributions. Using semgrep if you can translate "best practice" into a rule then it can automate the task for us.
-
-## Running locally
-
-Install semgrep using homebrew `brew install semgrep` or pip `pip install semgrep`.
-
-To run locally use following command:
-
-`semgrep --config=.github/helper/semgrep_rules [file/folder names]`
-
-## Testing
-semgrep allows testing the tests. Refer to this page: https://semgrep.dev/docs/writing-rules/testing-rules/
-
-When writing new rules you should write few positive and few negative cases as shown in the guide and current tests.
-
-To run current tests: `semgrep --test --test-ignore-todo .github/helper/semgrep_rules`
-
-
-## Reference
-
-If you are new to Semgrep read following pages to get started on writing/modifying rules:
-
-- https://semgrep.dev/docs/getting-started/
-- https://semgrep.dev/docs/writing-rules/rule-syntax
-- https://semgrep.dev/docs/writing-rules/pattern-examples/
-- https://semgrep.dev/docs/writing-rules/rule-ideas/#common-use-cases
diff --git a/.github/helper/semgrep_rules/frappe_correctness.py b/.github/helper/semgrep_rules/frappe_correctness.py
deleted file mode 100644
index 745e6463b8..0000000000
--- a/.github/helper/semgrep_rules/frappe_correctness.py
+++ /dev/null
@@ -1,64 +0,0 @@
-import frappe
-from frappe import _, flt
-
-from frappe.model.document import Document
-
-
-# ruleid: frappe-modifying-but-not-comitting
-def on_submit(self):
- if self.value_of_goods == 0:
- frappe.throw(_('Value of goods cannot be 0'))
- self.status = 'Submitted'
-
-
-# ok: frappe-modifying-but-not-comitting
-def on_submit(self):
- if self.value_of_goods == 0:
- frappe.throw(_('Value of goods cannot be 0'))
- self.status = 'Submitted'
- self.db_set('status', 'Submitted')
-
-# ok: frappe-modifying-but-not-comitting
-def on_submit(self):
- if self.value_of_goods == 0:
- frappe.throw(_('Value of goods cannot be 0'))
- x = "y"
- self.status = x
- self.db_set('status', x)
-
-
-# ok: frappe-modifying-but-not-comitting
-def on_submit(self):
- x = "y"
- self.status = x
- self.save()
-
-# ruleid: frappe-modifying-but-not-comitting-other-method
-class DoctypeClass(Document):
- def on_submit(self):
- self.good_method()
- self.tainted_method()
-
- def tainted_method(self):
- self.status = "uptate"
-
-
-# ok: frappe-modifying-but-not-comitting-other-method
-class DoctypeClass(Document):
- def on_submit(self):
- self.good_method()
- self.tainted_method()
-
- def tainted_method(self):
- self.status = "update"
- self.db_set("status", "update")
-
-# ok: frappe-modifying-but-not-comitting-other-method
-class DoctypeClass(Document):
- def on_submit(self):
- self.good_method()
- self.tainted_method()
- self.save()
-
- def tainted_method(self):
- self.status = "uptate"
diff --git a/.github/helper/semgrep_rules/frappe_correctness.yml b/.github/helper/semgrep_rules/frappe_correctness.yml
deleted file mode 100644
index 33a22fba6a..0000000000
--- a/.github/helper/semgrep_rules/frappe_correctness.yml
+++ /dev/null
@@ -1,146 +0,0 @@
-# This file specifies rules for correctness according to how frappe doctype data model works.
-
-rules:
-- id: frappe-modifying-but-not-comitting
- patterns:
- - pattern: |
- def $METHOD(self, ...):
- ...
- self.$ATTR = ...
- - pattern-not: |
- def $METHOD(self, ...):
- ...
- self.$ATTR = ...
- ...
- self.db_set(..., self.$ATTR, ...)
- - pattern-not: |
- def $METHOD(self, ...):
- ...
- self.$ATTR = $SOME_VAR
- ...
- self.db_set(..., $SOME_VAR, ...)
- - pattern-not: |
- def $METHOD(self, ...):
- ...
- self.$ATTR = $SOME_VAR
- ...
- self.save()
- - metavariable-regex:
- metavariable: '$ATTR'
- # this is negative look-ahead, add more attrs to ignore like (ignore|ignore_this_too|ignore_me)
- regex: '^(?!ignore_linked_doctypes|status_updater)(.*)$'
- - metavariable-regex:
- metavariable: "$METHOD"
- regex: "(on_submit|on_cancel)"
- message: |
- DocType modified in self.$METHOD. Please check if modification of self.$ATTR is commited to database.
- languages: [python]
- severity: ERROR
-
-- id: frappe-modifying-but-not-comitting-other-method
- patterns:
- - pattern: |
- class $DOCTYPE(...):
- def $METHOD(self, ...):
- ...
- self.$ANOTHER_METHOD()
- ...
-
- def $ANOTHER_METHOD(self, ...):
- ...
- self.$ATTR = ...
- - pattern-not: |
- class $DOCTYPE(...):
- def $METHOD(self, ...):
- ...
- self.$ANOTHER_METHOD()
- ...
-
- def $ANOTHER_METHOD(self, ...):
- ...
- self.$ATTR = ...
- ...
- self.db_set(..., self.$ATTR, ...)
- - pattern-not: |
- class $DOCTYPE(...):
- def $METHOD(self, ...):
- ...
- self.$ANOTHER_METHOD()
- ...
-
- def $ANOTHER_METHOD(self, ...):
- ...
- self.$ATTR = $SOME_VAR
- ...
- self.db_set(..., $SOME_VAR, ...)
- - pattern-not: |
- class $DOCTYPE(...):
- def $METHOD(self, ...):
- ...
- self.$ANOTHER_METHOD()
- ...
- self.save()
- def $ANOTHER_METHOD(self, ...):
- ...
- self.$ATTR = ...
- - metavariable-regex:
- metavariable: "$METHOD"
- regex: "(on_submit|on_cancel)"
- message: |
- self.$ANOTHER_METHOD is called from self.$METHOD, check if changes to self.$ATTR are commited to database.
- languages: [python]
- severity: ERROR
-
-- id: frappe-print-function-in-doctypes
- pattern: print(...)
- message: |
- Did you mean to leave this print statement in? Consider using msgprint or logger instead of print statement.
- languages: [python]
- severity: WARNING
- paths:
- include:
- - "*/**/doctype/*"
-
-- id: frappe-modifying-child-tables-while-iterating
- pattern-either:
- - pattern: |
- for $ROW in self.$TABLE:
- ...
- self.remove(...)
- - pattern: |
- for $ROW in self.$TABLE:
- ...
- self.append(...)
- message: |
- Child table being modified while iterating on it.
- languages: [python]
- severity: ERROR
- paths:
- include:
- - "*/**/doctype/*"
-
-- id: frappe-same-key-assigned-twice
- pattern-either:
- - pattern: |
- {..., $X: $A, ..., $X: $B, ...}
- - pattern: |
- dict(..., ($X, $A), ..., ($X, $B), ...)
- - pattern: |
- _dict(..., ($X, $A), ..., ($X, $B), ...)
- message: |
- key `$X` is uselessly assigned twice. This could be a potential bug.
- languages: [python]
- severity: ERROR
-
-- id: frappe-using-db-sql
- pattern-either:
- - pattern: frappe.db.sql(...)
- - pattern: frappe.db.sql_ddl(...)
- - pattern: frappe.db.sql_list(...)
- paths:
- exclude:
- - "test_*.py"
- message: |
- The PR contains a SQL query that may be re-written with frappe.qb (https://frappeframework.com/docs/user/en/api/query-builder) or the Database API (https://frappeframework.com/docs/user/en/api/database)
- languages: [python]
- severity: ERROR
diff --git a/.github/helper/semgrep_rules/security.py b/.github/helper/semgrep_rules/security.py
deleted file mode 100644
index f477d7c176..0000000000
--- a/.github/helper/semgrep_rules/security.py
+++ /dev/null
@@ -1,6 +0,0 @@
-def function_name(input):
- # ruleid: frappe-codeinjection-eval
- eval(input)
-
-# ok: frappe-codeinjection-eval
-eval("1 + 1")
diff --git a/.github/helper/semgrep_rules/security.yml b/.github/helper/semgrep_rules/security.yml
deleted file mode 100644
index 5a5098bf50..0000000000
--- a/.github/helper/semgrep_rules/security.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-rules:
-- id: frappe-codeinjection-eval
- patterns:
- - pattern-not: eval("...")
- - pattern: eval(...)
- message: |
- Detected the use of eval(). eval() can be dangerous if used to evaluate
- dynamic content. Avoid it or use safe_eval().
- languages: [python]
- severity: ERROR
-
-- id: frappe-sqli-format-strings
- patterns:
- - pattern-inside: |
- @frappe.whitelist()
- def $FUNC(...):
- ...
- - pattern-either:
- - pattern: frappe.db.sql("..." % ...)
- - pattern: frappe.db.sql(f"...", ...)
- - pattern: frappe.db.sql("...".format(...), ...)
- message: |
- Detected use of raw string formatting for SQL queries. This can lead to sql injection vulnerabilities. Refer security guidelines - https://github.com/frappe/erpnext/wiki/Code-Security-Guidelines
- languages: [python]
- severity: WARNING
diff --git a/.github/helper/semgrep_rules/translate.js b/.github/helper/semgrep_rules/translate.js
deleted file mode 100644
index 9cdfb75d0b..0000000000
--- a/.github/helper/semgrep_rules/translate.js
+++ /dev/null
@@ -1,44 +0,0 @@
-// ruleid: frappe-translation-empty-string
-__("")
-// ruleid: frappe-translation-empty-string
-__('')
-
-// ok: frappe-translation-js-formatting
-__('Welcome {0}, get started with ERPNext in just a few clicks.', [full_name]);
-
-// ruleid: frappe-translation-js-formatting
-__(`Welcome ${full_name}, get started with ERPNext in just a few clicks.`);
-
-// ok: frappe-translation-js-formatting
-__('This is fine');
-
-
-// ok: frappe-translation-trailing-spaces
-__('This is fine');
-
-// ruleid: frappe-translation-trailing-spaces
-__(' this is not ok ');
-// ruleid: frappe-translation-trailing-spaces
-__('this is not ok ');
-// ruleid: frappe-translation-trailing-spaces
-__(' this is not ok');
-
-// ok: frappe-translation-js-splitting
-__('You have {0} subscribers in your mailing list.', [subscribers.length])
-
-// todoruleid: frappe-translation-js-splitting
-__('You have') + subscribers.length + __('subscribers in your mailing list.')
-
-// ruleid: frappe-translation-js-splitting
-__('You have' + 'subscribers in your mailing list.')
-
-// ruleid: frappe-translation-js-splitting
-__('You have {0} subscribers' +
- 'in your mailing list', [subscribers.length])
-
-// ok: frappe-translation-js-splitting
-__("Ctrl+Enter to add comment")
-
-// ruleid: frappe-translation-js-splitting
-__('You have {0} subscribers \
- in your mailing list', [subscribers.length])
diff --git a/.github/helper/semgrep_rules/translate.py b/.github/helper/semgrep_rules/translate.py
deleted file mode 100644
index 9de6aa94f0..0000000000
--- a/.github/helper/semgrep_rules/translate.py
+++ /dev/null
@@ -1,61 +0,0 @@
-# Examples taken from https://frappeframework.com/docs/user/en/translations
-# This file is used for testing the tests.
-
-from frappe import _
-
-full_name = "Jon Doe"
-# ok: frappe-translation-python-formatting
-_('Welcome {0}, get started with ERPNext in just a few clicks.').format(full_name)
-
-# ruleid: frappe-translation-python-formatting
-_('Welcome %s, get started with ERPNext in just a few clicks.' % full_name)
-# ruleid: frappe-translation-python-formatting
-_('Welcome %(name)s, get started with ERPNext in just a few clicks.' % {'name': full_name})
-
-# ruleid: frappe-translation-python-formatting
-_('Welcome {0}, get started with ERPNext in just a few clicks.'.format(full_name))
-
-
-subscribers = ["Jon", "Doe"]
-# ok: frappe-translation-python-formatting
-_('You have {0} subscribers in your mailing list.').format(len(subscribers))
-
-# ruleid: frappe-translation-python-splitting
-_('You have') + len(subscribers) + _('subscribers in your mailing list.')
-
-# ruleid: frappe-translation-python-splitting
-_('You have {0} subscribers \
- in your mailing list').format(len(subscribers))
-
-# ok: frappe-translation-python-splitting
-_('You have {0} subscribers') \
- + 'in your mailing list'
-
-# ruleid: frappe-translation-trailing-spaces
-msg = _(" You have {0} pending invoice ")
-# ruleid: frappe-translation-trailing-spaces
-msg = _("You have {0} pending invoice ")
-# ruleid: frappe-translation-trailing-spaces
-msg = _(" You have {0} pending invoice")
-
-# ok: frappe-translation-trailing-spaces
-msg = ' ' + _("You have {0} pending invoices") + ' '
-
-# ruleid: frappe-translation-python-formatting
-_(f"can not format like this - {subscribers}")
-# ruleid: frappe-translation-python-splitting
-_(f"what" + f"this is also not cool")
-
-
-# ruleid: frappe-translation-empty-string
-_("")
-# ruleid: frappe-translation-empty-string
-_('')
-
-
-class Test:
- # ok: frappe-translation-python-splitting
- def __init__(
- args
- ):
- pass
diff --git a/.github/helper/semgrep_rules/translate.yml b/.github/helper/semgrep_rules/translate.yml
deleted file mode 100644
index 5f03fb9fd0..0000000000
--- a/.github/helper/semgrep_rules/translate.yml
+++ /dev/null
@@ -1,64 +0,0 @@
-rules:
-- id: frappe-translation-empty-string
- pattern-either:
- - pattern: _("")
- - pattern: __("")
- message: |
- Empty string is useless for translation.
- Please refer: https://frappeframework.com/docs/user/en/translations
- languages: [python, javascript, json]
- severity: ERROR
-
-- id: frappe-translation-trailing-spaces
- pattern-either:
- - pattern: _("=~/(^[ \t]+|[ \t]+$)/")
- - pattern: __("=~/(^[ \t]+|[ \t]+$)/")
- message: |
- Trailing or leading whitespace not allowed in translate strings.
- Please refer: https://frappeframework.com/docs/user/en/translations
- languages: [python, javascript, json]
- severity: ERROR
-
-- id: frappe-translation-python-formatting
- pattern-either:
- - pattern: _("..." % ...)
- - pattern: _("...".format(...))
- - pattern: _(f"...")
- message: |
- Only positional formatters are allowed and formatting should not be done before translating.
- Please refer: https://frappeframework.com/docs/user/en/translations
- languages: [python]
- severity: ERROR
-
-- id: frappe-translation-js-formatting
- patterns:
- - pattern: __(`...`)
- - pattern-not: __("...")
- message: |
- Template strings are not allowed for text formatting.
- Please refer: https://frappeframework.com/docs/user/en/translations
- languages: [javascript, json]
- severity: ERROR
-
-- id: frappe-translation-python-splitting
- pattern-either:
- - pattern: _(...) + _(...)
- - pattern: _("..." + "...")
- - pattern-regex: '[\s\.]_\([^\)]*\\\s*' # lines broken by `\`
- - pattern-regex: '[\s\.]_\(\s*\n' # line breaks allowed by python for using ( )
- message: |
- Do not split strings inside translate function. Do not concatenate using translate functions.
- Please refer: https://frappeframework.com/docs/user/en/translations
- languages: [python]
- severity: ERROR
-
-- id: frappe-translation-js-splitting
- pattern-either:
- - pattern-regex: '__\([^\)]*[\\]\s+'
- - pattern: __('...' + '...', ...)
- - pattern: __('...') + __('...')
- message: |
- Do not split strings inside translate function. Do not concatenate using translate functions.
- Please refer: https://frappeframework.com/docs/user/en/translations
- languages: [javascript, json]
- severity: ERROR
diff --git a/.github/helper/semgrep_rules/ux.js b/.github/helper/semgrep_rules/ux.js
deleted file mode 100644
index ae73f9cc60..0000000000
--- a/.github/helper/semgrep_rules/ux.js
+++ /dev/null
@@ -1,9 +0,0 @@
-
-// ok: frappe-missing-translate-function-js
-frappe.msgprint('{{ _("Both login and password required") }}');
-
-// ruleid: frappe-missing-translate-function-js
-frappe.msgprint('What');
-
-// ok: frappe-missing-translate-function-js
-frappe.throw(' {{ _("Both login and password required") }}. ');
diff --git a/.github/helper/semgrep_rules/ux.py b/.github/helper/semgrep_rules/ux.py
deleted file mode 100644
index a00d3cd8ae..0000000000
--- a/.github/helper/semgrep_rules/ux.py
+++ /dev/null
@@ -1,31 +0,0 @@
-import frappe
-from frappe import msgprint, throw, _
-
-
-# ruleid: frappe-missing-translate-function-python
-throw("Error Occured")
-
-# ruleid: frappe-missing-translate-function-python
-frappe.throw("Error Occured")
-
-# ruleid: frappe-missing-translate-function-python
-frappe.msgprint("Useful message")
-
-# ruleid: frappe-missing-translate-function-python
-msgprint("Useful message")
-
-
-# ok: frappe-missing-translate-function-python
-translatedmessage = _("Hello")
-
-# ok: frappe-missing-translate-function-python
-throw(translatedmessage)
-
-# ok: frappe-missing-translate-function-python
-msgprint(translatedmessage)
-
-# ok: frappe-missing-translate-function-python
-msgprint(_("Helpful message"))
-
-# ok: frappe-missing-translate-function-python
-frappe.throw(_("Error occured"))
diff --git a/.github/helper/semgrep_rules/ux.yml b/.github/helper/semgrep_rules/ux.yml
deleted file mode 100644
index dd667f36c0..0000000000
--- a/.github/helper/semgrep_rules/ux.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-rules:
-- id: frappe-missing-translate-function-python
- pattern-either:
- - patterns:
- - pattern: frappe.msgprint("...", ...)
- - pattern-not: frappe.msgprint(_("..."), ...)
- - patterns:
- - pattern: frappe.throw("...", ...)
- - pattern-not: frappe.throw(_("..."), ...)
- message: |
- All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
- languages: [python]
- severity: ERROR
-
-- id: frappe-missing-translate-function-js
- pattern-either:
- - patterns:
- - pattern: frappe.msgprint("...", ...)
- - pattern-not: frappe.msgprint(__("..."), ...)
- # ignore microtemplating e.g. msgprint("{{ _("server side translation") }}")
- - pattern-not: frappe.msgprint("=~/\{\{.*\_.*\}\}/i", ...)
- - patterns:
- - pattern: frappe.throw("...", ...)
- - pattern-not: frappe.throw(__("..."), ...)
- # ignore microtemplating
- - pattern-not: frappe.throw("=~/\{\{.*\_.*\}\}/i", ...)
- message: |
- All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
- languages: [javascript]
- severity: ERROR
diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml
index e27b406df0..325411cf5c 100644
--- a/.github/workflows/semgrep.yml
+++ b/.github/workflows/semgrep.yml
@@ -9,10 +9,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
+
+ - name: Download Semgrep rules
+ run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
+
- uses: returntocorp/semgrep-action@v1
env:
SEMGREP_TIMEOUT: 120
with:
config: >-
r/python.lang.correctness
- .github/helper/semgrep_rules
+ ./frappe-semgrep-rules/rules
diff --git a/cypress/integration/awesome_bar.js b/cypress/integration/awesome_bar.js
index fb09b384a8..8e503cce46 100644
--- a/cypress/integration/awesome_bar.js
+++ b/cypress/integration/awesome_bar.js
@@ -7,12 +7,13 @@ context('Awesome Bar', () => {
beforeEach(() => {
cy.get('.navbar .navbar-home').click();
+ cy.findByPlaceholderText('Search or type a command (Ctrl + G)').clear();
});
it('navigates to doctype list', () => {
- cy.findByPlaceholderText('Search or type a command (Ctrl + G)').type('todo', { delay: 200 });
+ cy.findByPlaceholderText('Search or type a command (Ctrl + G)').type('todo', { delay: 700 });
cy.get('.awesomplete').findByRole('listbox').should('be.visible');
- cy.findByPlaceholderText('Search or type a command (Ctrl + G)').type('{downarrow}{enter}', { delay: 100 });
+ cy.findByPlaceholderText('Search or type a command (Ctrl + G)').type('{downarrow}{enter}', { delay: 700 });
cy.get('.title-text').should('contain', 'To Do');
@@ -21,7 +22,7 @@ context('Awesome Bar', () => {
it('find text in doctype list', () => {
cy.findByPlaceholderText('Search or type a command (Ctrl + G)')
- .type('test in todo{downarrow}{enter}', { delay: 200 });
+ .type('test in todo{downarrow}{enter}', { delay: 700 });
cy.get('.title-text').should('contain', 'To Do');
@@ -31,14 +32,14 @@ context('Awesome Bar', () => {
it('navigates to new form', () => {
cy.findByPlaceholderText('Search or type a command (Ctrl + G)')
- .type('new blog post{downarrow}{enter}', { delay: 200 });
+ .type('new blog post{downarrow}{enter}', { delay: 700 });
cy.get('.title-text:visible').should('have.text', 'New Blog Post');
});
it('calculates math expressions', () => {
cy.findByPlaceholderText('Search or type a command (Ctrl + G)')
- .type('55 + 32{downarrow}{enter}', { delay: 200 });
+ .type('55 + 32{downarrow}{enter}', { delay: 700 });
cy.get('.modal-title').should('contain', 'Result');
cy.get('.msgprint').should('contain', '55 + 32 = 87');
diff --git a/cypress/integration/control_link.js b/cypress/integration/control_link.js
index 7d44a71d06..2a81338c59 100644
--- a/cypress/integration/control_link.js
+++ b/cypress/integration/control_link.js
@@ -49,19 +49,19 @@ context('Control Link', () => {
it('should unset invalid value', () => {
get_dialog_with_link().as('dialog');
- cy.intercept('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link');
+ cy.intercept('GET', '/api/method/frappe.client.get_value*').as('get_value');
cy.get('.frappe-control[data-fieldname=link] input')
.type('invalid value', { delay: 100 })
.blur();
- cy.wait('@validate_link');
+ cy.wait('@get_value');
cy.get('.frappe-control[data-fieldname=link] input').should('have.value', '');
});
it('should route to form on arrow click', () => {
get_dialog_with_link().as('dialog');
- cy.intercept('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link');
+ cy.intercept('GET', '/api/method/frappe.client.get_value*').as('get_value');
cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
cy.get('@todos').then(todos => {
@@ -69,7 +69,7 @@ context('Control Link', () => {
cy.get('@input').focus();
cy.wait('@search_link');
cy.get('@input').type(todos[0]).blur();
- cy.wait('@validate_link');
+ cy.wait('@get_value');
cy.get('@input').focus();
cy.findByTitle('Open Link')
.should('be.visible')
@@ -77,4 +77,19 @@ context('Control Link', () => {
cy.location('pathname').should('eq', `/app/todo/${todos[0]}`);
});
});
+
+ it('should fetch valid value', () => {
+ cy.get('@todos').then(todos => {
+ cy.visit(`/app/todo/${todos[0]}`);
+ cy.intercept('GET', '/api/method/frappe.client.get_value*').as('get_value');
+
+ cy.get('.frappe-control[data-fieldname=assigned_by] input').focus().as('input');
+ cy.get('@input').type('Administrator', {delay: 100}).blur();
+ cy.wait('@get_value');
+ cy.get('.frappe-control[data-fieldname=assigned_by_full_name] .control-value').should(
+ 'contain', 'Administrator'
+ );
+ });
+ });
+
});
diff --git a/cypress/integration/recorder.js b/cypress/integration/recorder.js
index 7a62b2e6d9..caf1349e6e 100644
--- a/cypress/integration/recorder.js
+++ b/cypress/integration/recorder.js
@@ -13,13 +13,6 @@ context('Recorder', () => {
});
});
- it('Navigate to Recorder', () => {
- cy.visit('/app');
- cy.awesomebar('recorder');
- cy.findByTitle('Recorder').should('exist');
- cy.url().should('include', '/recorder/detail');
- });
-
it('Recorder Empty State', () => {
cy.findByTitle('Recorder').should('exist');
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 6484370946..64a3b18b2f 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -241,7 +241,7 @@ Cypress.Commands.add('get_table_field', (tablefieldname, row_idx, fieldname, fie
});
Cypress.Commands.add('awesomebar', text => {
- cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, {delay: 100});
+ cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, {delay: 700});
});
Cypress.Commands.add('new_form', doctype => {
@@ -354,4 +354,4 @@ Cypress.Commands.add('click_listview_primary_button', (btn_name) => {
Cypress.Commands.add('click_timeline_action_btn', (btn_name) => {
cy.get('.timeline-message-box .custom-actions > .btn').contains(btn_name).click();
-});
\ No newline at end of file
+});
diff --git a/dev-requirements.txt b/dev-requirements.txt
new file mode 100644
index 0000000000..df3ae9484a
--- /dev/null
+++ b/dev-requirements.txt
@@ -0,0 +1,3 @@
+Faker~=8.1.0
+pyngrok~=5.0.5
+unittest-xml-reporting~=3.0.4
diff --git a/esbuild/esbuild.js b/esbuild/esbuild.js
index af2ffd3fc5..18de95b40d 100644
--- a/esbuild/esbuild.js
+++ b/esbuild/esbuild.js
@@ -46,7 +46,7 @@ let argv = yargs
})
.option("live-reload", {
type: "boolean",
- description: `Automatically reload web pages when assets are rebuilt.
+ description: `Automatically reload Desk when assets are rebuilt.
Can only be used with the --watch flag.`
})
.option("production", {
@@ -288,10 +288,24 @@ function get_watch_config() {
assets_json,
prev_assets_json
} = await write_assets_json(result.metafile);
+
+ let changed_files;
if (prev_assets_json) {
- log_rebuilt_assets(prev_assets_json, assets_json);
+ changed_files = get_rebuilt_assets(
+ prev_assets_json,
+ assets_json
+ );
+
+ let timestamp = new Date().toLocaleTimeString();
+ let message = `${timestamp}: Compiled ${changed_files.length} files...`;
+ log(chalk.yellow(message));
+ for (let filepath of changed_files) {
+ let filename = path.basename(filepath);
+ log(" " + filename);
+ }
+ log();
}
- notify_redis({ success: true });
+ notify_redis({ success: true, changed_files });
}
}
};
@@ -461,7 +475,7 @@ function run_build_command_for_apps(apps) {
process.chdir(cwd);
}
-async function notify_redis({ error, success }) {
+async function notify_redis({ error, success, changed_files }) {
// notify redis which in turns tells socketio to publish this to browser
let subscriber = get_redis_subscriber("redis_socketio");
subscriber.on("error", _ => {
@@ -484,6 +498,7 @@ async function notify_redis({ error, success }) {
if (success) {
payload = {
success: true,
+ changed_files,
live_reload: argv["live-reload"]
};
}
@@ -514,7 +529,7 @@ function open_in_editor() {
subscriber.subscribe("open_in_editor");
}
-function log_rebuilt_assets(prev_assets, new_assets) {
+function get_rebuilt_assets(prev_assets, new_assets) {
let added_files = [];
let old_files = Object.values(prev_assets);
let new_files = Object.values(new_assets);
@@ -524,17 +539,5 @@ function log_rebuilt_assets(prev_assets, new_assets) {
added_files.push(filepath);
}
}
-
- log(
- chalk.yellow(
- `${new Date().toLocaleTimeString()}: Compiled ${
- added_files.length
- } files...`
- )
- );
- for (let filepath of added_files) {
- let filename = path.basename(filepath);
- log(" " + filename);
- }
- log();
+ return added_files;
}
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 1b4429d55b..c8245b0bf0 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -30,9 +30,6 @@ from .utils.lazy_loader import lazy_import
from frappe.query_builder import get_query_builder, patch_query_execute
-# Lazy imports
-faker = lazy_import('faker')
-
__version__ = '14.0.0-dev'
__title__ = "Frappe Framework"
@@ -1838,6 +1835,7 @@ def parse_json(val):
return parse_json(val)
def mock(type, size=1, locale='en'):
+ import faker
results = []
fake = faker.Faker(locale)
if type not in dir(fake):
diff --git a/frappe/automation/workspace/tools/tools.json b/frappe/automation/workspace/tools/tools.json
index f556be1c07..fa2606dc43 100644
--- a/frappe/automation/workspace/tools/tools.json
+++ b/frappe/automation/workspace/tools/tools.json
@@ -1,20 +1,13 @@
{
- "category": "",
"charts": [],
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"ToDo\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Note\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"File\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Assignment Rule\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Auto Repeat\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tools\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Email\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Automation\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Event Streaming\", \"col\": 4}}]",
"creation": "2020-03-02 14:53:24.980279",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "tool",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Tools",
"links": [
{
@@ -215,15 +208,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:16:02.839180",
+ "modified": "2021-08-05 12:16:02.839181",
"modified_by": "Administrator",
"module": "Automation",
"name": "Tools",
- "onboarding": "",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
diff --git a/frappe/build.py b/frappe/build.py
index 8b32b03d60..6b93b8b93a 100644
--- a/frappe/build.py
+++ b/frappe/build.py
@@ -16,7 +16,6 @@ from frappe.utils.minify import JavascriptMinify
import click
import psutil
from urllib.parse import urlparse
-from simple_chalk import green
from semantic_version import Version
from requests import head
from requests.exceptions import HTTPError
@@ -108,7 +107,7 @@ def fetch_assets(url, frappe_head):
if not assets_archive:
raise AssetsNotDownloadedError(f"Assets could not be retrived from {url}")
- print(f"\n{green('✔')} Downloaded Frappe assets from {url}")
+ click.echo(click.style("✔", fg="green") + f" Downloaded Frappe assets from {url}")
return assets_archive
@@ -131,7 +130,7 @@ def setup_assets(assets_archive):
directories_created.add(asset_directory)
tar.makefile(file, dest)
- print("{0} Restored {1}".format(green('✔'), show))
+ click.echo(click.style("✔", fg="green") + f" Restored {show}")
return directories_created
@@ -379,7 +378,7 @@ def make_asset_dirs(hard_link=False):
except Exception:
print(fail_message, end="\r")
- print(unstrip(f"{green('✔')} Application Assets Linked") + "\n")
+ click.echo(unstrip(click.style("✔", fg="green") + " Application Assets Linked") + "\n")
def link_assets_dir(source, target, hard_link=False):
diff --git a/frappe/client.py b/frappe/client.py
index 21d10e8271..0e9be0a7ee 100644
--- a/frappe/client.py
+++ b/frappe/client.py
@@ -258,6 +258,12 @@ def set_default(key, value, parent=None):
frappe.db.set_default(key, value, parent or frappe.session.user)
frappe.clear_cache(user=frappe.session.user)
+@frappe.whitelist()
+def get_default(key, parent=None):
+ """set a user default value"""
+ return frappe.db.get_default(key, parent)
+
+
@frappe.whitelist(methods=['POST', 'PUT'])
def make_width_property_setter(doc):
'''Set width Property Setter
@@ -276,18 +282,17 @@ def bulk_update(docs):
docs = json.loads(docs)
failed_docs = []
for doc in docs:
+ doc.pop("flags", None)
try:
- ddoc = {key: val for key, val in doc.items() if key not in ['doctype', 'docname']}
- doctype = doc['doctype']
- docname = doc['docname']
- doc = frappe.get_doc(doctype, docname)
- doc.update(ddoc)
- doc.save()
- except:
+ existing_doc = frappe.get_doc(doc["doctype"], doc["docname"])
+ existing_doc.update(doc)
+ existing_doc.save()
+ except Exception:
failed_docs.append({
'doc': doc,
'exc': frappe.utils.get_traceback()
})
+
return {'failed_docs': failed_docs}
@frappe.whitelist()
diff --git a/frappe/core/doctype/activity_log/activity_log.py b/frappe/core/doctype/activity_log/activity_log.py
index 183a1c264c..69565a2c2a 100644
--- a/frappe/core/doctype/activity_log/activity_log.py
+++ b/frappe/core/doctype/activity_log/activity_log.py
@@ -7,6 +7,9 @@ from frappe.utils import get_fullname, now
from frappe.model.document import Document
from frappe.core.utils import set_timeline_doc
import frappe
+from frappe.query_builder import DocType, Interval
+from frappe.query_builder.functions import Now
+from pypika.terms import PseudoColumn
class ActivityLog(Document):
def before_insert(self):
@@ -44,6 +47,7 @@ def clear_activity_logs(days=None):
if not days:
days = 90
-
- frappe.db.sql("""delete from `tabActivity Log` where \
- creation< (NOW() - INTERVAL '{0}' DAY)""".format(days))
\ No newline at end of file
+ doctype = DocType("Activity Log")
+ frappe.db.delete(doctype, filters=(
+ doctype.creation < PseudoColumn(f"({Now() - Interval(days=days)})")
+ ))
\ No newline at end of file
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 5a91016e32..738fb73a34 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -87,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 = [
@@ -177,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'
@@ -315,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:
@@ -355,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
diff --git a/frappe/core/doctype/log_settings/log_settings.json b/frappe/core/doctype/log_settings/log_settings.json
index 8a2596b35c..f06d14f16b 100644
--- a/frappe/core/doctype/log_settings/log_settings.json
+++ b/frappe/core/doctype/log_settings/log_settings.json
@@ -49,7 +49,7 @@
"label": "Clear Activity Log After"
},
{
- "default": "90",
+ "default": "30",
"description": "In Days",
"fieldname": "clear_email_queue_after",
"fieldtype": "Int",
@@ -80,4 +80,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/core/doctype/log_settings/log_settings.py b/frappe/core/doctype/log_settings/log_settings.py
index c505302c52..5c9bc6c265 100644
--- a/frappe/core/doctype/log_settings/log_settings.py
+++ b/frappe/core/doctype/log_settings/log_settings.py
@@ -5,6 +5,10 @@
import frappe
from frappe import _
from frappe.model.document import Document
+from frappe.query_builder import DocType, Interval
+from frappe.query_builder.functions import Now
+from pypika.terms import PseudoColumn
+
class LogSettings(Document):
def clear_logs(self):
@@ -13,9 +17,10 @@ class LogSettings(Document):
self.clear_email_queue()
def clear_error_logs(self):
- frappe.db.sql(""" DELETE FROM `tabError Log`
- WHERE `creation` < (NOW() - INTERVAL '{0}' DAY)
- """.format(self.clear_error_log_after))
+ table = DocType("Error Log")
+ frappe.db.delete(table, filters=(
+ table.creation < PseudoColumn(f"({Now() - Interval(days=self.clear_error_log_after)})")
+ ))
def clear_activity_logs(self):
from frappe.core.doctype.activity_log.activity_log import clear_activity_logs
diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py
index 6a54314667..be0346d869 100644
--- a/frappe/core/doctype/report/report.py
+++ b/frappe/core/doctype/report/report.py
@@ -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/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py
index de858327a9..100e3c2790 100644
--- a/frappe/core/doctype/server_script/test_server_script.py
+++ b/frappe/core/doctype/server_script/test_server_script.py
@@ -122,7 +122,7 @@ 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):
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index e4b94cdbb6..45f7d47a27 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -16,6 +16,7 @@ from frappe.utils.user import get_system_managers
from frappe.website.utils import is_signup_disabled
from frappe.rate_limiter import rate_limit
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")
@@ -366,15 +367,21 @@ class User(Document):
# delete shares
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)
@@ -421,9 +428,10 @@ class User(Document):
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"""
diff --git a/frappe/core/doctype/user_type/user_type.py b/frappe/core/doctype/user_type/user_type.py
index 79a90933e7..c1fd678141 100644
--- a/frappe/core/doctype/user_type/user_type.py
+++ b/frappe/core/doctype/user_type/user_type.py
@@ -195,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/workspace/build/build.json b/frappe/core/workspace/build/build.json
index 8536c807d2..aabb4f9d1c 100644
--- a/frappe/core/workspace/build/build.json
+++ b/frappe/core/workspace/build/build.json
@@ -1,21 +1,13 @@
{
- "cards_label": "Elements",
- "category": "",
"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": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "tool",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Build",
"links": [
{
@@ -230,15 +222,12 @@
"type": "Link"
}
],
- "modified": "2021-09-05 21:14:52.384815",
+ "modified": "2021-09-05 21:14:52.384816",
"modified_by": "Administrator",
"module": "Core",
"name": "Build",
- "onboarding": "",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
diff --git a/frappe/core/workspace/settings/settings.json b/frappe/core/workspace/settings/settings.json
index 93a6c81c90..917ce2cbdc 100644
--- a/frappe/core/workspace/settings/settings.json
+++ b/frappe/core/workspace/settings/settings.json
@@ -1,20 +1,13 @@
{
- "category": "",
"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}}]",
+ "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": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "setting",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Settings",
"links": [
{
@@ -374,15 +367,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:16:03.456173",
+ "modified": "2021-08-05 12:16:03.456174",
"modified_by": "Administrator",
"module": "Core",
"name": "Settings",
- "onboarding": "",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
@@ -407,6 +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 09a835ea2c..85c110151b 100644
--- a/frappe/core/workspace/users/users.json
+++ b/frappe/core/workspace/users/users.json
@@ -1,20 +1,13 @@
{
- "category": "",
"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": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "users",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Users",
"links": [
{
@@ -152,15 +145,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:16:03.010204",
+ "modified": "2021-08-05 12:16:03.010205",
"modified_by": "Administrator",
"module": "Core",
"name": "Users",
- "onboarding": "",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py
index bf606701da..8c22d3c45c 100644
--- a/frappe/custom/doctype/custom_field/custom_field.py
+++ b/frappe/custom/doctype/custom_field/custom_field.py
@@ -131,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)
@@ -146,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 9633f0eb8a..ad3cf27eea 100644
--- a/frappe/custom/doctype/custom_field/test_custom_field.py
+++ b/frappe/custom/doctype/custom_field/test_custom_field.py
@@ -6,7 +6,42 @@
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/workspace/customization/customization.json b/frappe/custom/workspace/customization/customization.json
index 136b1a57eb..7aec530604 100644
--- a/frappe/custom/workspace/customization/customization.json
+++ b/frappe/custom/workspace/customization/customization.json
@@ -1,20 +1,13 @@
{
- "category": "",
"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": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "customization",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Customization",
"links": [
{
@@ -130,15 +123,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:15:57.486112",
+ "modified": "2021-08-05 12:15:57.486113",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customization",
- "onboarding": "",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
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/database/database.py b/frappe/database/database.py
index 3695c1c18b..c0d377fd42 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -37,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
@@ -83,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, run=True):
+ 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.
@@ -184,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)
@@ -239,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:
@@ -247,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
@@ -268,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:
@@ -330,7 +335,7 @@ class Database(object):
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.
@@ -357,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.
@@ -388,7 +396,7 @@ class Database(object):
if isinstance(filters, list):
order_by = order_by or "modified_desc"
- out = self._get_value_for_many_names(doctype, filters, fieldname, debug=debug)
+ out = self._get_value_for_many_names(doctype, filters, fieldname, debug=debug, run=run)
else:
fields = fieldname
@@ -401,26 +409,28 @@ class Database(object):
if (filters is not None) and (filters!=doctype or doctype=="DocType"):
try:
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)
+ 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,
@@ -449,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)
@@ -528,7 +539,8 @@ 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):
+ def _get_values_from_table(self, fields, filters, doctype, as_dict, debug, order_by=None,
+ update=None, for_update=False, run=True):
field_objects = []
for field in fields:
@@ -537,7 +549,9 @@ class Database(object):
else:
field_objects.append(field)
- criterion = self.query.build_conditions(table=doctype, filters=filters, orderby=order_by, for_update=for_update)
+ 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)
@@ -545,18 +559,17 @@ class Database(object):
if fields=="*":
query = criterion.select(fields)
as_dict = True
- r = self.sql(query, 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 {}
@@ -601,7 +614,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)
diff --git a/frappe/database/query.py b/frappe/database/query.py
index 7d7de85646..3545efb412 100644
--- a/frappe/database/query.py
+++ b/frappe/database/query.py
@@ -156,7 +156,7 @@ class Query:
Returns:
frappe.qb: condition object
"""
- condition = self.get_condition(table, **kwargs)
+ condition = self.add_conditions(self.get_condition(table, **kwargs), **kwargs)
return condition.where(criterion)
def add_conditions(self, conditions: frappe.qb, **kwargs):
diff --git a/frappe/defaults.py b/frappe/defaults.py
index 75feabc332..eb98db449f 100644
--- a/frappe/defaults.py
+++ b/frappe/defaults.py
@@ -4,6 +4,7 @@
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,14 +117,11 @@ 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)):
+ 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
@@ -191,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/desktop.py b/frappe/desk/desktop.py
index b9b01d0a74..e1789852f1 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -32,9 +32,6 @@ class Workspace:
self.page_name = page.get('name')
self.page_title = page.get('title')
self.public_page = page.get('public')
- self.extended_links = []
- self.extended_charts = []
- self.extended_shortcuts = []
self.workspace_manager = "Workspace Manager" in frappe.get_roles()
self.user = frappe.get_user()
@@ -151,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
@@ -187,17 +169,14 @@ 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()
}
@@ -249,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 = []
@@ -289,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):
@@ -311,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()
@@ -380,8 +352,7 @@ def get_desktop_page(page):
'charts': wspace.charts,
'shortcuts': wspace.shortcuts,
'cards': wspace.cards,
- 'onboardings': wspace.onboardings,
- 'allow_customization': not wspace.doc.disable_user_customization
+ 'onboardings': wspace.onboardings
}
except DoesNotExistError:
frappe.log_error(frappe.get_traceback())
@@ -414,7 +385,7 @@ def get_wspace_sidebar_items():
# Filter Page based on Permission
for page in all_pages:
try:
- wspace = Workspace(page)
+ wspace = Workspace(page, True)
if wspace.is_permitted() and wspace.is_page_allowed() or has_access:
if page.public:
pages.append(page)
@@ -461,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=
@@ -482,85 +452,6 @@ 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
-
- Args:
- page (stirng): Page name
-
- 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
-
-@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
-
- try:
- if page_doc.is_new():
- page_doc.insert(ignore_permissions=True)
- else:
- page_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)
-
- # Error log body
- log = \
- """
- page: {0}
- config: {1}
- exception: {2}
- """.format(page, json_config, e)
- frappe.log_error(log, _("Could not save customization"))
- return False
-
- return True
-
def save_new_widget(doc, page, blocks, new_widgets):
widgets = _dict(loads(new_widgets))
@@ -593,6 +484,7 @@ def save_new_widget(doc, page, blocks, new_widgets):
return False
return True
+
def clean_up(original_page, blocks):
page_widgets = {}
@@ -670,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/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 756a40da4b..04975c69e3 100644
--- a/frappe/desk/doctype/workspace/workspace.json
+++ b/frappe/desk/doctype/workspace/workspace.json
@@ -11,32 +11,19 @@
"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",
"roles_section",
"roles"
@@ -63,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",
@@ -74,7 +60,6 @@
"fieldtype": "Link",
"label": "Restrict to Domain",
"options": "Domain",
- "read_only_depends_on": "eval:doc.extends_another_page == 0",
"search_index": 1
},
{
@@ -89,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",
@@ -161,40 +88,12 @@
"fieldtype": "Section Break",
"label": "Link Cards"
},
- {
- "default": "0",
- "fieldname": "is_standard",
- "fieldtype": "Check",
- "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",
- "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",
@@ -213,21 +112,14 @@
"label": "Links",
"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"
+ "label": "Public",
+ "search_index": 1
},
{
"fieldname": "title",
@@ -266,7 +158,7 @@
],
"in_create": 1,
"links": [],
- "modified": "2021-09-16 12:01:06.450621",
+ "modified": "2021-09-16 12:01:06.450622",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace",
diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py
index a0a22a43fc..94114e3918 100644
--- a/frappe/desk/doctype/workspace/workspace.py
+++ b/frappe/desk/doctype/workspace/workspace.py
@@ -13,8 +13,8 @@ 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)
try:
@@ -23,15 +23,8 @@ class Workspace(Document):
except Exception:
frappe.throw(_("Content data shoud be a list"))
- 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."))
-
def on_update(self):
- if disable_saving_as_standard():
+ if disable_saving_as_public():
return
if frappe.conf.developer_mode and self.module and self.public:
@@ -39,12 +32,7 @@ class Workspace(Document):
@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] }
@@ -76,35 +64,6 @@ 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')
-
- for idx, name in enumerate(order):
- card = widgets[name].copy()
- links = loads(card.get('links'))
-
- self.append('links', {
- "label": card.get('label'),
- "type": "Card Break",
- "icon": card.get('icon'),
- "hidden": card.get('hidden') or False
- })
-
- for link in links:
- self.append('links', {
- "label": link.get('label'),
- "type": "Link",
- "link_type": link.get('link_type'),
- "link_to": link.get('link_to'),
- "onboard": link.get('onboard'),
- "only_for": link.get('only_for'),
- "dependencies": link.get('dependencies'),
- "is_query_report": link.get('is_query_report')
- })
-
def build_links_table_from_card(self, config):
for idx, card in enumerate(config):
@@ -137,7 +96,7 @@ class Workspace(Document):
"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 \
@@ -212,7 +171,7 @@ def save_page(title, icon, parent, public, sb_public_items, sb_private_items, de
def delete_pages(deleted_pages):
for page in deleted_pages:
- if page.get("public") and "Workspace Manager" not in frappe.get_roles():
+ 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")):
@@ -227,7 +186,7 @@ def sort_pages(sb_public_items, sb_private_items):
if sb_private_items:
sort_page(wspace_private_pages, sb_private_items)
- if sb_public_items and "Workspace Manager" in frappe.get_roles():
+ if sb_public_items and is_workspace_manager():
sort_page(wspace_public_pages, sb_public_items)
def sort_page(wspace_pages, pages):
@@ -242,3 +201,6 @@ def sort_page(wspace_pages, pages):
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/form/utils.py b/frappe/desk/form/utils.py
index 1c954edff0..291767de10 100644
--- a/frappe/desk/form/utils.py
+++ b/frappe/desk/form/utils.py
@@ -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):
diff --git a/frappe/desk/listview.py b/frappe/desk/listview.py
index e733adf868..43ad104f0d 100644
--- a/frappe/desk/listview.py
+++ b/frappe/desk/listview.py
@@ -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/integrations/workspace/integrations/integrations.json b/frappe/integrations/workspace/integrations/integrations.json
index 4167858db2..b85056e3ef 100644
--- a/frappe/integrations/workspace/integrations/integrations.json
+++ b/frappe/integrations/workspace/integrations/integrations.json
@@ -1,20 +1,13 @@
{
- "category": "",
"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": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "integration",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Integrations",
"links": [
{
@@ -267,15 +260,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:16:00.355267",
+ "modified": "2021-08-05 12:16:00.355268",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Integrations",
- "onboarding": "",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index 8f0e0aaefc..44f1398cc7 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -35,7 +35,7 @@ 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) -> List:
+ run=True, strict=True, pluck=None, ignore_ddl=False) -> 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):
@@ -87,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
@@ -104,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)
@@ -137,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()
diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py
index 14f1dbf2b0..de83b24cd8 100644
--- a/frappe/model/rename_doc.py
+++ b/frappe/model/rename_doc.py
@@ -458,7 +458,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 +471,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/modules/export_file.py b/frappe/modules/export_file.py
index 17e84ee488..ab6ffd4985 100644
--- a/frappe/modules/export_file.py
+++ b/frappe/modules/export_file.py
@@ -24,7 +24,7 @@ def write_document_file(doc, record_module=None, create_init=True, folder_name=N
doc_export = doc.as_dict(no_nulls=True)
doc.run_method("before_export", doc_export)
- strip_default_fields(doc, doc_export)
+ doc_export = strip_default_fields(doc, doc_export)
module = record_module or get_module_name(doc)
# create folder
@@ -42,12 +42,17 @@ def write_document_file(doc, record_module=None, create_init=True, folder_name=N
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'):
@@ -59,8 +64,6 @@ def write_code_files(folder, fname, doc, doc_export):
# remove from exporting
del doc_export[key]
-
-
def get_module_name(doc):
if doc.doctype == 'Module Def':
module = doc.name
diff --git a/frappe/patches.txt b/frappe/patches.txt
index 41ca1a1724..85df031073 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -182,4 +182,4 @@ 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 # 25.08.2021
+frappe.patches.v14_0.update_workspace2 # 20.09.2021
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/v14_0/update_workspace2.py b/frappe/patches/v14_0/update_workspace2.py
index c212faee76..82076c4328 100644
--- a/frappe/patches/v14_0/update_workspace2.py
+++ b/frappe/patches/v14_0/update_workspace2.py
@@ -4,8 +4,8 @@ from frappe import _
def execute():
frappe.reload_doc('desk', 'doctype', 'workspace', force=True)
- order_by = "pin_to_top desc, pin_to_bottom asc, name asc"
- for seq, wspace in enumerate(frappe.get_all('Workspace', order_by=order_by)):
+
+ 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)
@@ -53,7 +53,7 @@ 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
+ doc.public = 0 if doc.for_user else 1
doc.title = doc.extends or doc.label
doc.extends = ''
doc.category = ''
diff --git a/frappe/permissions.py b/frappe/permissions.py
index a086c73920..29651b4145 100644
--- a/frappe/permissions.py
+++ b/frappe/permissions.py
@@ -6,7 +6,7 @@ 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")
@@ -330,8 +330,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:
@@ -348,10 +347,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)
@@ -460,10 +462,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)
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 eeaef28393..67c0d236e0 100644
--- a/frappe/printing/doctype/letter_head/letter_head.py
+++ b/frappe/printing/doctype/letter_head/letter_head.py
@@ -2,7 +2,7 @@
# 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'''
+
+ {{ help_message }} +
+{{ doc.name }}
+{{ value }}
+| + {{ column.label }} + | + {% endfor %} +
|---|
| + {{ row.get_formatted(column.fieldname) }} + | + {% endfor %} +