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 d9603e89aa..0000000000 --- a/.github/helper/semgrep_rules/frappe_correctness.yml +++ /dev/null @@ -1,133 +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 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/try-on-f-cloud-button.svg b/.github/try-on-f-cloud-button.svg new file mode 100644 index 0000000000..fe0bb2c52d --- /dev/null +++ b/.github/try-on-f-cloud-button.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/README.md b/README.md index f8a1907da2..ef471aa05a 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,12 @@ Full-stack web application framework that uses Python and MariaDB on the server side and a tightly integrated client side library. Built for [ERPNext](https://erpnext.com) +
+ + + +
+ ## Table of Contents * [Installation](#installation) * [Contributing](#contributing) @@ -46,6 +52,7 @@ Full-stack web application framework that uses Python and MariaDB on the server * [Install via Docker](https://github.com/frappe/frappe_docker) * [Install via Frappe Bench](https://github.com/frappe/bench) * [Offical Documentation](https://frappeframework.com/docs/user/en/installation) +* [Managed Hosting on Frappe Cloud](https://frappecloud.com/deploy?apps=frappe&source=frappe_readme) ## Contributing diff --git a/codecov.yml b/codecov.yml index eeba1ff381..a9f6df0296 100644 --- a/codecov.yml +++ b/codecov.yml @@ -3,6 +3,7 @@ codecov: coverage: status: + patch: off project: default: false server: @@ -10,11 +11,6 @@ coverage: threshold: 0.5% flags: - server - ui-tests: - target: auto - threshold: 0.5% - flags: - - ui-tests comment: layout: "diff, flags" @@ -28,4 +24,4 @@ flags: ui-tests: paths: - ".*\\.js" - carryforward: true \ No newline at end of file + carryforward: true 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/integration/relative_time_filters.js b/cypress/integration/relative_time_filters.js index cbb0524c24..362d3a219b 100644 --- a/cypress/integration/relative_time_filters.js +++ b/cypress/integration/relative_time_filters.js @@ -1,44 +1,47 @@ -context('Relative Timeframe', () => { - before(() => { - cy.login(); - cy.visit('/app/website'); - cy.window().its('frappe').then(frappe => { - frappe.call("frappe.tests.ui_test_helpers.create_todo_records"); - }); - }); - it('sets relative timespan filter for last week and filters list', () => { - cy.visit('/app/List/ToDo/List'); - cy.clear_filters(); - cy.get('.list-row:contains("this is fourth todo")').should('exist'); - cy.add_filter(); - cy.get('.fieldname-select-area').should('exist'); - cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 }); - cy.get('select.condition.form-control').select("Timespan"); - cy.get('.filter-field select.input-with-feedback.form-control').select("last week"); - cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh'); - cy.get('.filter-popover .apply-filters').click({ force: true }); - cy.wait('@list_refresh'); - cy.get('.list-row-container').its('length').should('eq', 1); - cy.get('.list-row-container').should('contain', 'this is second todo'); - cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save') - .as('save_user_settings'); - cy.clear_filters(); - cy.wait('@save_user_settings'); - }); - it('sets relative timespan filter for next week and filters list', () => { - cy.visit('/app/List/ToDo/List'); - cy.clear_filters(); - cy.get('.list-row:contains("this is fourth todo")').should('exist'); - cy.add_filter(); - cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 }); - cy.get('select.condition.form-control').select("Timespan"); - cy.get('.filter-field select.input-with-feedback.form-control').select("next week"); - cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh'); - cy.get('.filter-popover .apply-filters').click({ force: true }); - cy.wait('@list_refresh'); - cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save') - .as('save_user_settings'); - cy.clear_filters(); - cy.wait('@save_user_settings'); - }); -}); +// TODO: Enable this again +// currently this is flaky possibly because of different timezone in CI + +// context('Relative Timeframe', () => { +// before(() => { +// cy.login(); +// cy.visit('/app/website'); +// cy.window().its('frappe').then(frappe => { +// frappe.call("frappe.tests.ui_test_helpers.create_todo_records"); +// }); +// }); +// it('sets relative timespan filter for last week and filters list', () => { +// cy.visit('/app/List/ToDo/List'); +// cy.clear_filters(); +// cy.get('.list-row:contains("this is fourth todo")').should('exist'); +// cy.add_filter(); +// cy.get('.fieldname-select-area').should('exist'); +// cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 }); +// cy.get('select.condition.form-control').select("Timespan"); +// cy.get('.filter-field select.input-with-feedback.form-control').select("last week"); +// cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh'); +// cy.get('.filter-popover .apply-filters').click({ force: true }); +// cy.wait('@list_refresh'); +// cy.get('.list-row-container').its('length').should('eq', 1); +// cy.get('.list-row-container').should('contain', 'this is second todo'); +// cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save') +// .as('save_user_settings'); +// cy.clear_filters(); +// cy.wait('@save_user_settings'); +// }); +// it('sets relative timespan filter for next week and filters list', () => { +// cy.visit('/app/List/ToDo/List'); +// cy.clear_filters(); +// cy.get('.list-row:contains("this is fourth todo")').should('exist'); +// cy.add_filter(); +// cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 }); +// cy.get('select.condition.form-control').select("Timespan"); +// cy.get('.filter-field select.input-with-feedback.form-control').select("next week"); +// cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh'); +// cy.get('.filter-popover .apply-filters').click({ force: true }); +// cy.wait('@list_refresh'); +// cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save') +// .as('save_user_settings'); +// cy.clear_filters(); +// cy.wait('@save_user_settings'); +// }); +// }); diff --git a/cypress/integration/timeline.js b/cypress/integration/timeline.js index 3071330b61..191b5a2b2c 100644 --- a/cypress/integration/timeline.js +++ b/cypress/integration/timeline.js @@ -50,8 +50,8 @@ context('Timeline', () => { cy.click_modal_primary_button('Yes'); //Deleting the added ToDo - cy.get('.menu-btn-group [data-original-title="Menu"]').click(); - cy.get('.menu-btn-group .dropdown-item').contains('Delete').click(); + cy.get('[id="page-ToDo"] .menu-btn-group [data-original-title="Menu"]').click(); + cy.get('[id="page-ToDo"] .menu-btn-group .dropdown-item').contains('Delete').click(); cy.findByRole('button', {name: 'Yes'}).click(); }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 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 bf4436358e..18de95b40d 100644 --- a/esbuild/esbuild.js +++ b/esbuild/esbuild.js @@ -44,6 +44,11 @@ let argv = yargs type: "boolean", description: "Run in watch mode and rebuild on file changes" }) + .option("live-reload", { + type: "boolean", + description: `Automatically reload Desk when assets are rebuilt. + Can only be used with the --watch flag.` + }) .option("production", { type: "boolean", description: "Run build in production mode" @@ -283,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 }); } } }; @@ -456,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", _ => { @@ -478,7 +497,9 @@ async function notify_redis({ error, success }) { } if (success) { payload = { - success: true + success: true, + changed_files, + live_reload: argv["live-reload"] }; } @@ -508,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); @@ -518,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 64e445973f..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" @@ -1480,7 +1477,10 @@ def get_value(*args, **kwargs): def as_json(obj, indent=1): from frappe.utils.response import json_handler - return json.dumps(obj, indent=indent, sort_keys=True, default=json_handler, separators=(',', ': ')) + try: + return json.dumps(obj, indent=indent, sort_keys=True, default=json_handler, separators=(',', ': ')) + except TypeError: + return json.dumps(obj, indent=indent, default=json_handler, separators=(',', ': ')) def are_emails_muted(): from frappe.utils import cint @@ -1835,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 05fa213018..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 @@ -257,6 +256,13 @@ def watch(apps=None): if apps: command += " --apps {apps}".format(apps=apps) + live_reload = frappe.utils.cint( + os.environ.get("LIVE_RELOAD", frappe.conf.live_reload) + ) + + if live_reload: + command += " --live-reload" + check_node_executable() frappe_app_path = frappe.get_app_path("frappe", "..") frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env()) @@ -372,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/communication/communication.py b/frappe/core/doctype/communication/communication.py index 5714d122eb..db7269bbe2 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -256,7 +256,7 @@ class Communication(Document, CommunicationEmailMixin): def set_delivery_status(self, commit=False): '''Look into the status of Email Queue linked to this Communication and set the Delivery Status of this Communication''' delivery_status = None - status_counts = Counter(frappe.db.sql_list('''select status from `tabEmail Queue` where communication=%s''', self.name)) + status_counts = Counter(frappe.get_all("Email Queue", pluck="status", filters={"communication": self.name})) if self.sent_or_received == "Received": return diff --git a/frappe/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py index 52cd370890..b6d8070d00 100644 --- a/frappe/core/doctype/communication/mixins.py +++ b/frappe/core/doctype/communication/mixins.py @@ -217,17 +217,7 @@ class CommunicationEmailMixin: if not emails: return [] - disabled_users = frappe.db.sql_list(""" - SELECT - email - FROM - `tabUser` - where - email in %(emails)s - and - thread_notify=0 - """, {'emails': tuple(emails)}) - return disabled_users + return frappe.get_all("User", pluck="email", filters={"email": ["in", emails], "thread_notify": 0}) @staticmethod def filter_disabled_users(emails): @@ -236,17 +226,7 @@ class CommunicationEmailMixin: if not emails: return [] - disabled_users = frappe.db.sql_list(""" - SELECT - email - FROM - `tabUser` - where - email in %(emails)s - and - enabled=0 - """, {'emails': tuple(emails)}) - return disabled_users + return frappe.get_all("User", pluck="email", filters={"email": ["in", emails], "enabled": 0}) def sendmail_input_dict(self, print_html=None, print_format=None, send_me_a_copy=None, print_letterhead=None, is_inbound_mail_communcation=None): diff --git a/frappe/core/doctype/data_export/exporter.py b/frappe/core/doctype/data_export/exporter.py index 7c660c7180..c5cf67ba57 100644 --- a/frappe/core/doctype/data_export/exporter.py +++ b/frappe/core/doctype/data_export/exporter.py @@ -261,6 +261,7 @@ class DataExporter: self.writer.writerow([self.data_keys.data_separator]) def add_data(self): + from frappe.query_builder import DocType if self.template and not self.with_data: return @@ -305,9 +306,15 @@ class DataExporter: if self.all_doctypes: # add child tables for c in self.child_doctypes: - for ci, child in enumerate(frappe.db.sql("""select * from `tab{0}` - where parent=%s and parentfield=%s order by idx""".format(c['doctype']), - (doc.name, c['parentfield']), as_dict=1)): + child_doctype_table = DocType(c["doctype"]) + data_row = ( + frappe.qb.from_(child_doctype_table) + .select("*") + .where(child_doctype_table.parent == doc.name) + .where(child_doctype_table.parentfield == c["parentfield"]) + .orderby(child_doctype_table.idx) + ) + for ci, child in enumerate(data_row.run()): self.add_data_row(rows, c['doctype'], c['parentfield'], child, ci) for row in rows: diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 8f8a8ed287..5a91016e32 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -23,6 +23,7 @@ from frappe.modules.import_file import get_file_path from frappe.model.meta import Meta from frappe.desk.utils import validate_route_conflict from frappe.website.utils import clear_cache +from frappe.query_builder.functions import Concat class InvalidFieldNameError(frappe.ValidationError): pass class UniqueFieldnameError(frappe.ValidationError): pass @@ -465,7 +466,7 @@ class DocType(Document): return # check if atleast 1 record exists - if not (frappe.db.table_exists(self.name) and frappe.db.sql("select name from `tab{}` limit 1".format(self.name))): + if not (frappe.db.table_exists(self.name) and frappe.get_all(self.name, fields=["name"], limit=1, as_list=True)): return existing_property_setter = frappe.db.get_value("Property Setter", {"doc_type": self.name, @@ -571,17 +572,17 @@ class DocType(Document): def make_amendable(self): """If is_submittable is set, add amended_from docfields.""" if self.is_submittable: - if not frappe.db.sql("""select name from tabDocField - where fieldname = 'amended_from' and parent = %s""", self.name): - self.append("fields", { - "label": "Amended From", - "fieldtype": "Link", - "fieldname": "amended_from", - "options": self.name, - "read_only": 1, - "print_hide": 1, - "no_copy": 1 - }) + docfield_exists = frappe.get_all("DocField", filters={"fieldname": "amended_from", "parent": self.name}, pluck="name", limit=1) + if not docfield_exists: + self.append("fields", { + "label": "Amended From", + "fieldtype": "Link", + "fieldname": "amended_from", + "options": self.name, + "read_only": 1, + "print_hide": 1, + "no_copy": 1 + }) def make_repeatable(self): """If allow_auto_repeat is set, add auto_repeat custom field.""" @@ -706,12 +707,13 @@ def validate_series(dt, autoname=None, name=None): and (not autoname.startswith('format:')): prefix = autoname.split('.')[0] - used_in = frappe.db.sql(""" - SELECT `name` - FROM `tabDocType` - WHERE `autoname` LIKE CONCAT(%s, '.%%') - AND `name`!=%s - """, (prefix, name)) + doctype = frappe.qb.DocType("DocType") + used_in = (frappe.qb + .from_(doctype) + .select(doctype.name) + .where(doctype.autoname.like(Concat(prefix,".%"))) + .where(doctype.name != name) + ).run() if used_in: frappe.throw(_("Series {0} already used in {1}").format(prefix, used_in[0][0])) diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py index 4538ffb6bb..9a758b53f5 100644 --- a/frappe/core/doctype/file/test_file.py +++ b/frappe/core/doctype/file/test_file.py @@ -204,10 +204,14 @@ class TestFile(unittest.TestCase): def delete_test_data(self): - for f in frappe.db.sql('''select name, file_name from tabFile where - is_home_folder = 0 and is_attachments_folder = 0 order by creation desc'''): - frappe.delete_doc("File", f[0]) - + test_file_data = frappe.db.get_all( + "File", + pluck="name", + filters={"is_home_folder": 0, "is_attachments_folder": 0}, + order_by="creation desc", + ) + for f in test_file_data: + frappe.delete_doc("File", f) def upload_file(self): _file = frappe.get_doc({ diff --git a/frappe/core/doctype/language/language.json b/frappe/core/doctype/language/language.json index eed29883c1..9ab8f55f6b 100644 --- a/frappe/core/doctype/language/language.json +++ b/frappe/core/doctype/language/language.json @@ -7,6 +7,7 @@ "document_type": "Setup", "engine": "InnoDB", "field_order": [ + "enabled", "language_code", "language_name", "flag", @@ -39,15 +40,22 @@ "fieldtype": "Link", "label": "Based On", "options": "Language" + }, + { + "default": "1", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" } ], "icon": "fa fa-globe", "in_create": 1, "links": [], - "modified": "2020-04-16 22:11:33.066852", + "modified": "2021-10-18 14:02:06.818219", "modified_by": "Administrator", "module": "Core", "name": "Language", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { 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 8a471b9173..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 @@ -38,7 +43,7 @@ def has_unseen_error_log(user): 'message': _("You have unseen {0}").format(' Error Logs ') } - if frappe.db.sql_list("select name from `tabError Log` where seen = 0 limit 1"): + if frappe.get_all("Error Log", filters={"seen": 0}, limit=1): log_settings = frappe.get_cached_doc('Log Settings') if log_settings.users_to_notify: diff --git a/frappe/core/doctype/navbar_settings/navbar_settings.py b/frappe/core/doctype/navbar_settings/navbar_settings.py index fd8db31d10..46eb5c3e7a 100644 --- a/frappe/core/doctype/navbar_settings/navbar_settings.py +++ b/frappe/core/doctype/navbar_settings/navbar_settings.py @@ -22,7 +22,6 @@ class NavbarSettings(Document): if not frappe.flags.in_patch and (len(before_save_items) > len(after_save_items)): frappe.throw(_("Please hide the standard navbar items instead of deleting them")) -@frappe.whitelist(allow_guest=True) def get_app_logo(): app_logo = frappe.db.get_single_value('Navbar Settings', 'app_logo', cache=True) if not app_logo: diff --git a/frappe/core/doctype/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/transaction_log/transaction_log.py b/frappe/core/doctype/transaction_log/transaction_log.py index bb94642f48..e2e75b130c 100644 --- a/frappe/core/doctype/transaction_log/transaction_log.py +++ b/frappe/core/doctype/transaction_log/transaction_log.py @@ -14,10 +14,9 @@ class TransactionLog(Document): self.row_index = index self.timestamp = now_datetime() if index != 1: - prev_hash = frappe.db.sql( - "SELECT `chaining_hash` FROM `tabTransaction Log` WHERE `row_index` = '{0}'".format(index - 1)) + prev_hash = frappe.get_all("Transaction Log", filters={"row_index":str(index-1)}, pluck="chaining_hash", limit=1) if prev_hash: - self.previous_hash = prev_hash[0][0] + self.previous_hash = prev_hash[0] else: self.previous_hash = "Indexing broken" else: diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 1d5f89897d..cd7dcd6a34 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -202,7 +202,8 @@ "fieldname": "role_profile_name", "fieldtype": "Link", "label": "Role Profile", - "options": "Role Profile" + "options": "Role Profile", + "permlevel": 1 }, { "fieldname": "roles_html", @@ -670,7 +671,7 @@ } ], "max_attachments": 5, - "modified": "2021-02-02 16:11:06.037543", + "modified": "2021-10-18 16:56:05.578379", "modified_by": "Administrator", "module": "Core", "name": "User", 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_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py index 66ffd48822..1366ace115 100644 --- a/frappe/core/doctype/user_permission/user_permission.py +++ b/frappe/core/doctype/user_permission/user_permission.py @@ -54,7 +54,7 @@ class UserPermission(Document): ref_link = frappe.get_desk_link(self.doctype, overlap_exists[0].name) frappe.throw(_("{0} has already assigned default value for {1}.").format(ref_link, self.allow)) -@frappe.whitelist(allow_guest=True) +@frappe.whitelist() def get_user_permissions(user=None): '''Get all users permissions for the user as a dict of doctype''' # if this is called from client-side, 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 e98cc22f41..df5ad6dfda 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. @@ -178,6 +180,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) @@ -233,7 +238,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: @@ -241,7 +246,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 @@ -262,7 +267,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: @@ -324,7 +329,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. @@ -351,12 +356,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. @@ -382,7 +390,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 @@ -395,26 +403,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, @@ -443,8 +453,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) @@ -522,7 +533,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: @@ -531,7 +543,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) @@ -539,18 +553,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 {} @@ -595,7 +608,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/load.py b/frappe/desk/form/load.py index d276a9707f..89e6598859 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -13,7 +13,7 @@ from frappe.desk.form.document_follow import is_document_followed from frappe import _ from urllib.parse import quote -@frappe.whitelist(allow_guest=True) +@frappe.whitelist() def getdoc(doctype, name, user=None): """ Loads a doclist for a given document. This method is called directly from the client. @@ -52,7 +52,7 @@ def getdoc(doctype, name, user=None): frappe.response.docs.append(doc) -@frappe.whitelist(allow_guest=True) +@frappe.whitelist() def getdoctype(doctype, with_parent=False, cached_timestamp=None): """load doctype""" 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 f079205cb0..43ad104f0d 100644 --- a/frappe/desk/listview.py +++ b/frappe/desk/listview.py @@ -2,7 +2,7 @@ # License: MIT. See LICENSE import frappe -@frappe.whitelist(allow_guest=True) +@frappe.whitelist() def get_list_settings(doctype): try: return frappe.get_cached_doc("List View Settings", doctype) @@ -26,7 +26,7 @@ def get_group_by_count(doctype, current_filters, field): current_filters = frappe.parse_json(current_filters) subquery_condition = '' - subquery = frappe.get_all(doctype, filters=current_filters, return_query = True) + subquery = frappe.get_all(doctype, filters=current_filters, run=False) if field == 'assigned_to': subquery_condition = ' and `tabToDo`.reference_name in ({subquery})'.format(subquery = subquery) return frappe.db.sql("""select `tabToDo`.owner as name, count(*) as count diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 31eb224652..6c9fa2e937 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -14,7 +14,7 @@ from frappe.utils import cstr, format_duration from frappe.model.base_document import get_controller -@frappe.whitelist(allow_guest=True) +@frappe.whitelist() @frappe.read_only() def get(): args = get_form_params() diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py index 37089d58df..7081a84e7a 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.py +++ b/frappe/email/doctype/auto_email_report/auto_email_report.py @@ -249,7 +249,7 @@ def make_links(columns, data): if col.options and row.get(col.fieldname) and row.get(col.options): row[col.fieldname] = get_link_to_form(row[col.options], row[col.fieldname]) elif col.fieldtype == "Currency" and row.get(col.fieldname): - doc = frappe.get_doc(col.parent, doc_name) if doc_name else None + doc = frappe.get_doc(col.parent, doc_name) if doc_name and col.parent else None # Pass the Document to get the currency based on docfield option row[col.fieldname] = frappe.format_value(row[col.fieldname], col, doc=doc) return columns, data diff --git a/frappe/exceptions.py b/frappe/exceptions.py index 31a94ac883..4b59f8f38f 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -99,8 +99,8 @@ class IncompatibleApp(ValidationError): pass class InvalidDates(ValidationError): pass class DataTooLongException(ValidationError): pass class FileAlreadyAttachedException(Exception): pass -class DocumentAlreadyRestored(Exception): pass -class AttachmentLimitReached(Exception): pass +class DocumentAlreadyRestored(ValidationError): pass +class AttachmentLimitReached(ValidationError): pass # OAuth exceptions class InvalidAuthorizationHeader(CSRFTokenError): pass class InvalidAuthorizationPrefix(CSRFTokenError): pass diff --git a/frappe/installer.py b/frappe/installer.py index 1fe891c852..d1a13fdaab 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -507,14 +507,13 @@ def convert_archive_content(sql_file_path): sql_file_path = Path(sql_file_path) os.rename(sql_file_path, old_sql_file_path) - sql_file_path.unlink(missing_ok=True) sql_file_path.touch() with open(old_sql_file_path) as r, open(sql_file_path, "a") as w: for line in r: w.write(line.replace("ROW_FORMAT=COMPRESSED", "ROW_FORMAT=DYNAMIC")) - old_sql_file_path.unlink(missing_ok=True) + old_sql_file_path.unlink() def extract_sql_gzip(sql_gz_path): diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py index 90927e13f8..9ccd1c0210 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py @@ -336,7 +336,6 @@ def dropbox_auth_finish(return_access_token=False): _("Dropbox access is approved!") + close, indicator_color='green') -@frappe.whitelist(allow_guest=True) def set_dropbox_access_token(access_token): frappe.db.set_value("Dropbox Settings", None, 'dropbox_access_token', access_token) frappe.db.commit() diff --git a/frappe/integrations/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 978f3062c5..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() @@ -597,8 +592,8 @@ class DatabaseQuery(object): self.conditions.append(self.get_share_condition()) else: - #if has if_owner permission skip user perm check - if role_permissions.get("has_if_owner_enabled") and role_permissions.get("if_owner", {}): + # skip user perm check if owner constraint is required + if requires_owner_constraint(role_permissions): self.match_conditions.append( f"`tab{self.doctype}`.`owner` = {frappe.db.escape(self.user, percent=False)}" ) @@ -895,3 +890,22 @@ def get_date_range(operator, value): timespan = period_map[operator] + ' ' + timespan_map[value] if operator != 'timespan' else value return get_timespan_date_range(timespan) + +def requires_owner_constraint(role_permissions): + """Returns True if "select" or "read" isn't available without being creator.""" + + if not role_permissions.get("has_if_owner_enabled"): + return + + if_owner_perms = role_permissions.get("if_owner") + if not if_owner_perms: + return + + # has select or read without if owner, no need for constraint + for perm_type in ("select", "read"): + if role_permissions.get(perm_type) and perm_type not in if_owner_perms: + return + + # not checking if either select or read if present in if_owner_perms + # because either of those is required to perform a query + return True diff --git a/frappe/model/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/patches.txt b/frappe/patches.txt index 37fa5379d6..078bdcd5b4 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -183,4 +183,4 @@ 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.copy_mail_data #08.03.21 +frappe.patches.v14_0.copy_mail_data #08.03.21 \ No newline at end of file diff --git a/frappe/patches/v11_0/remove_skip_for_doctype.py b/frappe/patches/v11_0/remove_skip_for_doctype.py index 638a5a0fd7..1063dca3ff 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(tuple(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 7ee1119ebb..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") @@ -107,13 +107,9 @@ def get_doc_permissions(doc, user=None, ptype=None): meta = frappe.get_meta(doc.doctype) def is_user_owner(): - doc_owner = doc.get('owner') or '' - doc_owner = doc_owner.lower() - session_user = frappe.session.user.lower() - return doc_owner == session_user + return (doc.get("owner") or "").lower() == frappe.session.user.lower() - - if has_controller_permissions(doc, ptype, user=user) == False : + if has_controller_permissions(doc, ptype, user=user) is False: push_perm_check_log('Not allowed via controller permission check') return {ptype: 0} @@ -182,22 +178,23 @@ def get_role_permissions(doctype_meta, user=None, is_owner=None): applicable_permissions = list(filter(is_perm_applicable, getattr(doctype_meta, 'permissions', []))) has_if_owner_enabled = any(p.get('if_owner', 0) for p in applicable_permissions) - perms['has_if_owner_enabled'] = has_if_owner_enabled for ptype in rights: pvalue = any(p.get(ptype, 0) for p in applicable_permissions) # check if any perm object allows perm type perms[ptype] = cint(pvalue) - if (pvalue - and has_if_owner_enabled - and not has_permission_without_if_owner_enabled(ptype) - and ptype != 'create'): + if ( + pvalue + and has_if_owner_enabled + and not has_permission_without_if_owner_enabled(ptype) + and ptype != 'create' + ): perms['if_owner'][ptype] = cint(pvalue and is_owner) # has no access if not owner # only provide select or read access so that user is able to at-least access list # (and the documents will be filtered based on owner sin further checks) - perms[ptype] = 1 if ptype in ['select', 'read'] else 0 + perms[ptype] = 1 if ptype in ('select', 'read') else 0 frappe.local.role_permissions[cache_key] = perms @@ -333,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: @@ -351,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) @@ -463,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''' +
+ {self.name} +
+ ''' frappe.msgprint(frappe._('Header HTML set from attachment {0}').format(self.image), alert = True) else: frappe.msgprint(frappe._('Please attach an image file to set HTML'), alert = True, indicator = 'orange') diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js index 7b7009dbaf..3fd1d9d148 100644 --- a/frappe/printing/doctype/print_format/print_format.js +++ b/frappe/printing/doctype/print_format/print_format.js @@ -30,7 +30,11 @@ frappe.ui.form.on("Print Format", { frappe.msgprint(__("Please select DocType first")); return; } - frappe.set_route("print-format-builder", frm.doc.name); + if (frm.doc.print_format_builder_beta) { + frappe.set_route("print-format-builder-beta", frm.doc.name); + } else { + frappe.set_route("print-format-builder", frm.doc.name); + } }); } else if (frm.doc.custom_format && !frm.doc.raw_printing) { diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json index 4032cef209..75ec0fa7fd 100644 --- a/frappe/printing/doctype/print_format/print_format.json +++ b/frappe/printing/doctype/print_format/print_format.json @@ -19,19 +19,26 @@ "html", "raw_commands", "section_break_9", + "margin_top", + "margin_bottom", + "margin_left", + "margin_right", "align_labels_right", "show_section_headings", "line_breaks", "absolute_value", "column_break_11", + "font_size", "font", + "page_number", "css_section", "css", "custom_html_help", "section_break_13", "print_format_help", "format_data", - "print_format_builder" + "print_format_builder", + "print_format_builder_beta" ], "fields": [ { @@ -149,12 +156,10 @@ "options": "Language" }, { - "default": "Default", "depends_on": "eval:!doc.custom_format", "fieldname": "font", - "fieldtype": "Select", - "label": "Font", - "options": "Default\nHelvetica Neue\nArial\nHelvetica\nVerdana\nMonospace" + "fieldtype": "Data", + "label": "Google Font" }, { "depends_on": "eval:!doc.raw_printing", @@ -205,16 +210,60 @@ "fieldname": "absolute_value", "fieldtype": "Check", "label": "Show Absolute Values" + }, + { + "default": "0", + "fieldname": "print_format_builder_beta", + "fieldtype": "Check", + "label": "Print Format Builder Beta" + }, + { + "default": "15", + "fieldname": "margin_top", + "fieldtype": "Float", + "label": "Margin Top" + }, + { + "default": "15", + "fieldname": "margin_bottom", + "fieldtype": "Float", + "label": "Margin Bottom" + }, + { + "default": "15", + "fieldname": "margin_left", + "fieldtype": "Float", + "label": "Margin Left" + }, + { + "default": "15", + "fieldname": "margin_right", + "fieldtype": "Float", + "label": "Margin Right" + }, + { + "default": "14", + "fieldname": "font_size", + "fieldtype": "Int", + "label": "Font Size" + }, + { + "default": "Hide", + "fieldname": "page_number", + "fieldtype": "Select", + "label": "Page Number", + "options": "Hide\nTop Left\nTop Center\nTop Right\nBottom Left\nBottom Center\nBottom Right" } ], "icon": "fa fa-print", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2021-03-01 15:25:46.578863", + "modified": "2021-10-12 17:52:41.167107", "modified_by": "Administrator", "module": "Printing", "name": "Print Format", + "naming_rule": "Set by user", "owner": "Administrator", "permissions": [ { diff --git a/frappe/printing/doctype/print_format/print_format.py b/frappe/printing/doctype/print_format/print_format.py index 878a864b38..f19c0af9bf 100644 --- a/frappe/printing/doctype/print_format/print_format.py +++ b/frappe/printing/doctype/print_format/print_format.py @@ -7,10 +7,24 @@ import frappe.utils import json from frappe import _ from frappe.utils.jinja import validate_template - +from frappe.utils.weasyprint import get_html, download_pdf from frappe.model.document import Document class PrintFormat(Document): + def onload(self): + templates = frappe.db.get_all( + "Print Format Field Template", + fields=["template", "field", "name"], + filters={"document_type": self.doc_type}, + ) + self.set_onload("print_templates", templates) + + def get_html(self, docname, letterhead=None): + return get_html(self.doc_type, docname, self.name, letterhead) + + def download_pdf(self, docname, letterhead=None): + return download_pdf(self.doc_type, docname, self.name, letterhead) + def validate(self): if (self.standard=="Yes" and not frappe.local.conf.get("developer_mode") @@ -38,6 +52,10 @@ class PrintFormat(Document): def extract_images(self): from frappe.core.doctype.file.file import extract_images_from_html + + if self.print_format_builder_beta: + return + if self.format_data: data = json.loads(self.format_data) for df in data: diff --git a/frappe/printing/doctype/print_format_field_template/__init__.py b/frappe/printing/doctype/print_format_field_template/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/printing/doctype/print_format_field_template/print_format_field_template.js b/frappe/printing/doctype/print_format_field_template/print_format_field_template.js new file mode 100644 index 0000000000..7fbb0d7359 --- /dev/null +++ b/frappe/printing/doctype/print_format_field_template/print_format_field_template.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Print Format Field Template', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/printing/doctype/print_format_field_template/print_format_field_template.json b/frappe/printing/doctype/print_format_field_template/print_format_field_template.json new file mode 100644 index 0000000000..3b79aae7e8 --- /dev/null +++ b/frappe/printing/doctype/print_format_field_template/print_format_field_template.json @@ -0,0 +1,101 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2021-10-05 14:23:56.508499", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "field", + "template_file", + "column_break_3", + "module", + "standard", + "section_break_5", + "template" + ], + "fields": [ + { + "depends_on": "eval:!doc.multiple", + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Document Type", + "mandatory_depends_on": "eval:!doc.multiple", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "field", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Default Template For Field" + }, + { + "depends_on": "eval:!doc.standard", + "fieldname": "template", + "fieldtype": "Code", + "label": "Template", + "mandatory_depends_on": "eval:!doc.standard", + "options": "HTML" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "depends_on": "standard", + "fieldname": "module", + "fieldtype": "Link", + "label": "Module", + "options": "Module Def" + }, + { + "default": "0", + "fieldname": "standard", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Standard" + }, + { + "depends_on": "eval:doc.standard", + "fieldname": "template_file", + "fieldtype": "Data", + "label": "Template File", + "mandatory_depends_on": "eval:doc.standard" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-10-19 17:47:59.577949", + "modified_by": "Administrator", + "module": "Printing", + "name": "Print Format Field Template", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/printing/doctype/print_format_field_template/print_format_field_template.py b/frappe/printing/doctype/print_format_field_template/print_format_field_template.py new file mode 100644 index 0000000000..b66afdb6b1 --- /dev/null +++ b/frappe/printing/doctype/print_format_field_template/print_format_field_template.py @@ -0,0 +1,43 @@ +# Copyright (c) 2021, Frappe Technologies and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from frappe import _ + + +class PrintFormatFieldTemplate(Document): + def validate(self): + if self.standard and not (frappe.conf.developer_mode or frappe.flags.in_patch): + frappe.throw(_("Enable developer mode to create a standard Print Template")) + + def before_insert(self): + self.validate_duplicate() + + def on_update(self): + self.validate_duplicate() + self.export_doc() + + def validate_duplicate(self): + if not self.standard: + return + if not self.field: + return + + filters = {"document_type": self.document_type, "field": self.field} + if not self.is_new(): + filters.update({"name": ("!=", self.name)}) + result = frappe.db.get_all("Print Format Field Template", filters=filters, limit=1) + if result: + frappe.throw( + _("A template already exists for field {0} of {1}").format( + frappe.bold(self.field), frappe.bold(self.document_type) + ), + frappe.DuplicateEntryError, + title=_("Duplicate Entry"), + ) + + def export_doc(self): + from frappe.modules.utils import export_module_json + + export_module_json(self, self.standard, self.module) diff --git a/frappe/printing/doctype/print_format_field_template/test_print_format_field_template.py b/frappe/printing/doctype/print_format_field_template/test_print_format_field_template.py new file mode 100644 index 0000000000..f0b1329763 --- /dev/null +++ b/frappe/printing/doctype/print_format_field_template/test_print_format_field_template.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies and Contributors +# See license.txt + +# import frappe +import unittest + +class TestPrintFormatFieldTemplate(unittest.TestCase): + pass diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js index 1e158c616e..f10c703589 100644 --- a/frappe/printing/page/print/print.js +++ b/frappe/printing/page/print/print.js @@ -41,7 +41,11 @@ frappe.ui.form.PrintView = class {
- ` + +
+ +
+ ` ); this.print_settings = frappe.model.get_doc( @@ -72,7 +76,7 @@ frappe.ui.form.PrintView = class { this.page.add_button( __('PDF'), - () => this.render_page('/api/method/frappe.utils.print_format.download_pdf?'), + () => this.render_pdf(), { icon: 'small-file' } ); @@ -134,7 +138,7 @@ frappe.ui.form.PrintView = class { add_sidebar_item(df, is_dynamic) { if (df.fieldtype == 'Select') { - df.input_class = 'btn btn-default btn-sm'; + df.input_class = 'btn btn-default btn-sm text-left'; } let field = frappe.ui.form.make_control({ @@ -190,6 +194,13 @@ frappe.ui.form.PrintView = class { this.set_breadcrumbs(); this.setup_customize_dialog(); + // print format builder beta + this.page.add_inner_message(` + + ${__('Try the new Print Format Builder')} + + `); + let tasks = [ this.refresh_print_options, this.set_default_print_language, @@ -233,7 +244,7 @@ frappe.ui.form.PrintView = class { let print_format = this.get_print_format(); let is_custom_format = print_format.name && - print_format.print_format_builder && + (print_format.print_format_builder || print_format.print_format_builder_beta) && print_format.standard === 'No'; let is_standard_but_editable = print_format.name && print_format.custom_format; @@ -243,7 +254,11 @@ frappe.ui.form.PrintView = class { return; } if (is_custom_format) { - frappe.set_route('print-format-builder', print_format.name); + if (print_format.print_format_builder_beta) { + frappe.set_route('print-format-builder-beta', print_format.name); + } else { + frappe.set_route('print-format-builder', print_format.name); + } return; } // start a new print format @@ -261,6 +276,11 @@ frappe.ui.form.PrintView = class { fieldtype: 'Read Only', default: print_format.name || 'Standard', }, + { + label: __('Use the new Print Format Builder'), + fieldname: 'beta', + fieldtype: 'Check' + }, ], (data) => { frappe.route_options = { @@ -268,6 +288,7 @@ frappe.ui.form.PrintView = class { doctype: this.frm.doctype, name: data.print_format_name, based_on: data.based_on, + beta: data.beta }; frappe.set_route('print-format-builder'); this.print_sel.val(data.print_format_name); @@ -380,6 +401,17 @@ frappe.ui.form.PrintView = class { } preview() { + let print_format = this.get_print_format(); + if (print_format.print_format_builder_beta) { + this.print_wrapper.find('.print-preview-wrapper').hide(); + this.print_wrapper.find('.preview-beta-wrapper').show(); + this.preview_beta(); + return; + } + + this.print_wrapper.find('.preview-beta-wrapper').hide(); + this.print_wrapper.find('.print-preview-wrapper').show(); + const $print_format = this.print_wrapper.find('iframe'); this.$print_format_body = $print_format.contents(); this.get_print_html((out) => { @@ -403,6 +435,21 @@ frappe.ui.form.PrintView = class { }); } + preview_beta() { + let print_format = this.get_print_format(); + const iframe = this.print_wrapper.find('.preview-beta-wrapper iframe'); + let params = new URLSearchParams({ + doctype: this.frm.doc.doctype, + name: this.frm.doc.name, + print_format: print_format.name + }); + let letterhead = this.get_letterhead(); + if (letterhead) { + params.append("letterhead", letterhead); + } + iframe.prop('src', `/printpreview?${params.toString()}`); + } + setup_print_format_dom(out, $print_format) { this.print_wrapper.find('.print-format-skeleton').remove(); let base_url = frappe.urllib.get_base_url(); @@ -565,6 +612,26 @@ frappe.ui.form.PrintView = class { }, }); } + + render_pdf() { + let print_format = this.get_print_format(); + if (print_format.print_format_builder_beta) { + let params = new URLSearchParams({ + doctype: this.frm.doc.doctype, + name: this.frm.doc.name, + print_format: print_format.name, + letterhead: this.get_letterhead() + }); + let w = window.open(`/api/method/frappe.utils.weasyprint.download_pdf?${params}`); + if (!w) { + frappe.msgprint(__('Please enable pop-ups')); + return; + } + } else { + this.render_page('/api/method/frappe.utils.print_format.download_pdf?'); + } + } + render_page(method, printit = false) { let w = window.open( frappe.urllib.get_full_url( diff --git a/frappe/printing/page/print_format_builder/print_format_builder.js b/frappe/printing/page/print_format_builder/print_format_builder.js index b73ff31d32..313e8da539 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder.js +++ b/frappe/printing/page/print_format_builder/print_format_builder.js @@ -12,9 +12,9 @@ frappe.pages['print-format-builder'].on_page_show = function(wrapper) { }); } else if(frappe.route_options) { if(frappe.route_options.make_new) { - let { doctype, name, based_on } = frappe.route_options; + let { doctype, name, based_on, beta } = frappe.route_options; frappe.route_options = null; - frappe.print_format_builder.setup_new_print_format(doctype, name, based_on); + frappe.print_format_builder.setup_new_print_format(doctype, name, based_on, beta); } else { frappe.print_format_builder.print_format = frappe.route_options.doc; frappe.route_options = null; @@ -126,18 +126,22 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder { }); } - setup_new_print_format(doctype, name, based_on) { + setup_new_print_format(doctype, name, based_on, beta) { frappe.call({ method: 'frappe.printing.page.print_format_builder.print_format_builder.create_custom_format', args: { doctype: doctype, name: name, - based_on: based_on + based_on: based_on, + beta: Boolean(beta) }, callback: (r) => { - if(!r.exc) { - if(r.message) { - this.print_format = r.message; + if (r.message) { + let print_format = r.message; + if (print_format.print_format_builder_beta) { + frappe.set_route('print-format-builder-beta', print_format.name); + } else { + this.print_format = print_format; this.refresh(); } } diff --git a/frappe/printing/page/print_format_builder/print_format_builder.py b/frappe/printing/page/print_format_builder/print_format_builder.py index d9f57762b0..fae564d3c3 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder.py +++ b/frappe/printing/page/print_format_builder/print_format_builder.py @@ -1,11 +1,16 @@ import frappe @frappe.whitelist() -def create_custom_format(doctype, name, based_on='Standard'): +def create_custom_format(doctype, name, based_on='Standard', beta=False): doc = frappe.new_doc('Print Format') doc.doc_type = doctype doc.name = name - doc.print_format_builder = 1 + beta = frappe.parse_json(beta) + + if beta: + doc.print_format_builder_beta = 1 + else: + doc.print_format_builder = 1 doc.format_data = frappe.db.get_value('Print Format', based_on, 'format_data') \ if based_on != 'Standard' else None doc.insert() diff --git a/frappe/printing/page/print_format_builder_beta/__init__.py b/frappe/printing/page/print_format_builder_beta/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.css b/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.css new file mode 100644 index 0000000000..0bd8d9c0f3 --- /dev/null +++ b/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.css @@ -0,0 +1,3 @@ +.layout-main-section-wrapper { + margin-bottom: 0; +} diff --git a/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.js b/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.js new file mode 100644 index 0000000000..e923bbcb00 --- /dev/null +++ b/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.js @@ -0,0 +1,122 @@ +frappe.pages["print-format-builder-beta"].on_page_load = function(wrapper) { + frappe.ui.make_app_page({ + parent: wrapper, + title: __("Print Format Builder"), + single_column: true + }); + + // hot reload in development + if (frappe.boot.developer_mode) { + frappe.hot_update = frappe.hot_update || []; + frappe.hot_update.push(() => load_print_format_builder_beta(wrapper)); + } +}; + +frappe.pages["print-format-builder-beta"].on_page_show = function(wrapper) { + load_print_format_builder_beta(wrapper); +}; + +function load_print_format_builder_beta(wrapper) { + let route = frappe.get_route(); + let $parent = $(wrapper).find(".layout-main-section"); + $parent.empty(); + + if (route.length > 1) { + frappe.require("print_format_builder.bundle.js").then(() => { + frappe.print_format_builder = new frappe.ui.PrintFormatBuilder({ + wrapper: $parent, + page: wrapper.page, + print_format: route[1] + }); + }); + } else { + let d = new frappe.ui.Dialog({ + title: __("Create or Edit Print Format"), + fields: [ + { + label: __("Action"), + fieldname: "action", + fieldtype: "Select", + options: [ + { label: __("Create New"), value: "Create" }, + { label: __("Edit Existing"), value: "Edit" } + ], + change() { + let action = d.get_value("action"); + d.get_primary_btn().text( + action === "Create" ? __("Create") : __("Edit") + ); + } + }, + { + label: __("Select Document Type"), + fieldname: "doctype", + fieldtype: "Link", + options: "DocType", + filters: { + istable: 0 + }, + reqd: 1, + default: frappe.route_options + ? frappe.route_options.doctype + : null + }, + { + label: __("Print Format Name"), + fieldname: "print_format_name", + fieldtype: "Data", + depends_on: doc => doc.action === "Create", + mandatory_depends_on: doc => doc.action === "Create" + }, + { + label: __("Select Print Format"), + fieldname: "print_format", + fieldtype: "Link", + options: "Print Format", + only_select: 1, + depends_on: doc => doc.action === "Edit", + get_query() { + return { + filters: { + doc_type: d.get_value("doctype"), + print_format_builder_beta: 1 + } + }; + }, + mandatory_depends_on: doc => doc.action === "Edit" + } + ], + primary_action_label: __("Edit"), + primary_action({ + action, + doctype, + print_format, + print_format_name + }) { + if (action === "Edit") { + frappe.set_route("print-format-builder-beta", print_format); + } else if (action === "Create") { + d.get_primary_btn().prop("disabled", true); + frappe.db + .insert({ + doctype: "Print Format", + name: print_format_name, + doc_type: doctype, + print_format_builder_beta: 1 + }) + .then(doc => { + frappe.set_route( + "print-format-builder-beta", + doc.name + ); + }) + .finally(() => { + d.get_primary_btn().prop("disabled", false); + }); + } + } + }); + d.set_value("action", "Create"); + d.show(); + } +} diff --git a/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.json b/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.json new file mode 100644 index 0000000000..a5b1288bc0 --- /dev/null +++ b/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.json @@ -0,0 +1,22 @@ +{ + "content": null, + "creation": "2021-07-10 12:22:16.138485", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2021-07-10 12:22:16.138485", + "modified_by": "Administrator", + "module": "Printing", + "name": "print-format-builder-beta", + "owner": "Administrator", + "page_name": "Print Format Builder Beta", + "roles": [ + { + "role": "System Manager" + } + ], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0 +} \ No newline at end of file diff --git a/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.py b/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.py new file mode 100644 index 0000000000..e13412cd07 --- /dev/null +++ b/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.py @@ -0,0 +1,17 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe +import functools + + +@frappe.whitelist() +def get_google_fonts(): + return _get_google_fonts() + + +@functools.lru_cache() +def _get_google_fonts(): + file_path = frappe.get_app_path("frappe", "data", "google_fonts.json") + return frappe.parse_json(frappe.read_file(file_path)) diff --git a/frappe/public/js/frappe/build_events/BuildSuccess.vue b/frappe/public/js/frappe/build_events/BuildSuccess.vue index 75a365fdc2..5ab40271bb 100644 --- a/frappe/public/js/frappe/build_events/BuildSuccess.vue +++ b/frappe/public/js/frappe/build_events/BuildSuccess.vue @@ -3,8 +3,11 @@ v-if="is_shown" class="flex justify-between build-success-message align-center" > -
Compiled successfully
- + Compiled successfully + Refresh @@ -14,11 +17,17 @@ export default { name: "BuildSuccess", data() { return { - is_shown: false + is_shown: false, + live_reload: false, }; }, methods: { - show() { + show(data) { + if (data.live_reload) { + this.live_reload = true; + this.reload(); + } + this.is_shown = true; if (this.timeout) { clearTimeout(this.timeout); diff --git a/frappe/public/js/frappe/build_events/build_events.bundle.js b/frappe/public/js/frappe/build_events/build_events.bundle.js index 6c8986af3f..21960f0b00 100644 --- a/frappe/public/js/frappe/build_events/build_events.bundle.js +++ b/frappe/public/js/frappe/build_events/build_events.bundle.js @@ -7,16 +7,45 @@ let error = null; frappe.realtime.on("build_event", data => { if (data.success) { + // remove executed cache for rebuilt files + let changed_files = data.changed_files; + if (Array.isArray(changed_files)) { + for (let file of changed_files) { + if (file.includes(".bundle.")) { + let parts = file.split(".bundle."); + if (parts.length === 2) { + let filename = parts[0].split("/").slice(-1)[0]; + + frappe.assets.executed_ = frappe.assets.executed_.filter( + asset => !asset.includes(`${filename}.bundle`) + ); + } + } + } + } + // update assets json + frappe.call("frappe.sessions.get_boot_assets_json").then(r => { + if (r.message) { + frappe.boot.assets_json = r.message; + + if (frappe.hot_update) { + frappe.hot_update.forEach(callback => { + callback(); + }); + } + } + }); show_build_success(data); } else if (data.error) { show_build_error(data); } }); -function show_build_success() { +function show_build_success(data) { if (error) { error.hide(); } + if (!success) { let target = $('
') .appendTo($container) @@ -27,7 +56,7 @@ function show_build_success() { }); success = vm.$children[0]; } - success.show(); + success.show(data); } function show_build_error(data) { diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index a4dc1a6709..a53368d67a 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -283,7 +283,7 @@ frappe.Application = class Application { frappe.workspaces = {}; for (let page of frappe.boot.allowed_workspaces || []) { frappe.modules[page.module]=page; - frappe.workspaces[frappe.router.slug(page.title)] = page; + frappe.workspaces[frappe.router.slug(page.name)] = page; } } diff --git a/frappe/public/js/frappe/form/controls/barcode.js b/frappe/public/js/frappe/form/controls/barcode.js index 04bdb3f19e..3a678b6a97 100644 --- a/frappe/public/js/frappe/form/controls/barcode.js +++ b/frappe/public/js/frappe/form/controls/barcode.js @@ -56,9 +56,6 @@ frappe.ui.form.ControlBarcode = class ControlBarcode extends frappe.ui.form.Cont get_options(value) { // get JsBarcode options let options = {}; - options.background = "var(--control-bg)"; - options.lineColor = "var(--text-color)"; - options.font = "var(--font-stack)"; options.fontSize = "16"; options.width = "3"; options.height = "50"; diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 83f3f8dd70..e7339372b3 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -451,51 +451,55 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat return this.validate_link_and_fetch(this.df, this.get_options(), this.docname, value); } - validate_link_and_fetch(df, doctype, docname, value) { - if(value) { - return new Promise((resolve) => { - var fetch = ''; - if(this.frm && this.frm.fetch_dict[df.fieldname]) { - fetch = this.frm.fetch_dict[df.fieldname].columns.join(', '); - } - // if default and no fetch, no need to validate - if (!fetch && df.__default_value && df.__default_value===value) { - resolve(value); - } + validate_link_and_fetch(df, options, docname, value) { + if (!value) return; - this.fetch_and_validate_link(resolve, df, doctype, docname, value, fetch); - }); - } - } + return new Promise((resolve) => { + const fetch_map = this.fetch_map; - fetch_and_validate_link(resolve, df, doctype, docname, value, fetch) { - frappe.call({ - method: 'frappe.desk.form.utils.validate_link', - type: "GET", - args: { - 'value': value, - 'options': doctype, - 'fetch': fetch - }, - no_spinner: true, - callback: (r) => { - if (r.message=='Ok') { - if (r.fetch_values && docname) { - this.set_fetch_values(df, docname, r.fetch_values); - } - resolve(r.valid_value); - } else { - resolve(""); - } + // if default and no fetch, no need to validate + if ($.isEmptyObject(fetch_map) && df.__default_value === value) { + return resolve(value); } + + frappe.db.get_value( + options, + value, + ["name", ...Object.values(fetch_map)], + (response) => { + if (!response.name) { + return resolve(""); + } + + if (docname) { + for (const [target_field, source_field] of Object.entries(fetch_map)) { + frappe.model.set_value( + df.parent, + docname, + target_field, + response[source_field], + df.fieldtype, + ); + } + } + + return resolve(response.name); + } + ) }); } - set_fetch_values(df, docname, fetch_values) { - var fl = this.frm.fetch_dict[df.fieldname].fields; - for(var i=0; i < fl.length; i++) { - frappe.model.set_value(df.parent, docname, fl[i], fetch_values[i], df.fieldtype); + get fetch_map() { + const fetch_map = {}; + if (!this.frm) return fetch_map; + + for (const key of ["*", this.df.parent]) { + if (this.frm.fetch_dict[key] && this.frm.fetch_dict[key][this.df.fieldname]) { + Object.assign(fetch_map, this.frm.fetch_dict[key][this.df.fieldname]); + } } + + return fetch_map; } }; diff --git a/frappe/public/js/frappe/form/footer/form_timeline.js b/frappe/public/js/frappe/form/footer/form_timeline.js index 128bd355ad..b9ec9a5438 100644 --- a/frappe/public/js/frappe/form/footer/form_timeline.js +++ b/frappe/public/js/frappe/form/footer/form_timeline.js @@ -302,7 +302,7 @@ class FormTimeline extends BaseTimeline { (this.doc_info.info_logs || []).forEach(info_log => { info_timeline_contents.push({ creation: info_log.creation, - content: `${this.get_user_link(info_log.comment_email)} ${info_log.content}`, + content: `${this.get_user_link(info_log.owner)} ${info_log.content}`, }); }); return info_timeline_contents; diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index a095956dfe..75d68b12db 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -1112,12 +1112,24 @@ frappe.ui.form.Form = class FrappeForm { } // UTILITIES - add_fetch(link_field, src_field, tar_field) { - if(!this.fetch_dict[link_field]) { - this.fetch_dict[link_field] = {'columns':[], 'fields':[]}; - } - this.fetch_dict[link_field].columns.push(src_field); - this.fetch_dict[link_field].fields.push(tar_field); + add_fetch(link_field, source_field, target_field, target_doctype) { + /* + Example fetch dict to get sender_email from email_id field in sender: + { + "Notification": { + "sender": { + "sender_email": "email_id" + } + } + } + */ + + if (!target_doctype) target_doctype = "*"; + + // Target field kept as key because source field could be non-unique + this.fetch_dict + .setDefault(target_doctype, {}) + .setDefault(link_field, {})[target_field] = source_field; } has_perm(ptype) { diff --git a/frappe/public/js/frappe/form/script_manager.js b/frappe/public/js/frappe/form/script_manager.js index f877a7cf8b..d1732ee702 100644 --- a/frappe/public/js/frappe/form/script_manager.js +++ b/frappe/public/js/frappe/form/script_manager.js @@ -196,7 +196,7 @@ frappe.ui.form.ScriptManager = class ScriptManager { 'Text Editor', 'Code', 'Link', 'Float', 'Int', 'Date', 'Select', 'Duration'].includes(df.fieldtype) || df.read_only==1) && df.fetch_from && df.fetch_from.indexOf(".")!=-1) { var parts = df.fetch_from.split("."); - me.frm.add_fetch(parts[0], parts[1], df.fieldname); + me.frm.add_fetch(parts[0], parts[1], df.fieldname, df.parent); } } diff --git a/frappe/public/js/frappe/list/bulk_operations.js b/frappe/public/js/frappe/list/bulk_operations.js index 931f2cf587..ee6e6d753c 100644 --- a/frappe/public/js/frappe/list/bulk_operations.js +++ b/frappe/public/js/frappe/list/bulk_operations.js @@ -78,7 +78,7 @@ export default class BulkOperations { args: { doctype: 'Letter Head', fields: ['name', 'is_default'], - limit: 0 + limit_page_length: 0 }, async: false, callback (r) { diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 09072e106e..07c8acef27 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -302,9 +302,20 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { refresh(refresh_header=false) { super.refresh().then(() => { this.render_header(refresh_header); + this.update_checkbox(); }); } + update_checkbox(target) { + let $check_all_checkbox = this.$checkbox_actions.find(".list-check-all"); + + if ($check_all_checkbox.prop("checked") && target && !target.prop("checked")) { + $check_all_checkbox.prop("checked", false); + } + + $check_all_checkbox.prop("checked", this.$checks.length === this.data.length); + } + setup_freeze_area() { this.$freeze = $( `
${__( @@ -1253,6 +1264,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { } this.$checkbox_cursor = $target; + + this.update_checkbox($target); }); } @@ -1398,6 +1411,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { this.$checkbox_actions.show(); this.$list_head_subject.hide(); } + this.update_checkbox(); this.toggle_actions_menu_button(this.$checks.length > 0); } diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js index cf132c82ea..7874ffcdde 100644 --- a/frappe/public/js/frappe/router.js +++ b/frappe/public/js/frappe/router.js @@ -58,6 +58,12 @@ $('body').on('click', 'a', function(e) { if (frappe.router.is_app_route(e.currentTarget.pathname)) { // target has "/app, this is a v2 style route. + + frappe.route_options = {}; + let params = new URLSearchParams(e.currentTarget.search); + for (const [key, value] of params) { + frappe.route_options[key] = value; + } return override(e.currentTarget.pathname + e.currentTarget.hash); } @@ -127,12 +133,14 @@ frappe.router = { // /app/user/user-001 = ["Form", "User", "user-001"] // /app/event/view/calendar/default = ["List", "Event", "Calendar", "Default"] + let private_wspace = route[1] && `${route[1]}-${frappe.user.name.toLowerCase()}`; + if (frappe.workspaces[route[0]]) { // public workspace route = ['Workspaces', frappe.workspaces[route[0]].title]; - } else if (route[0] == 'private' && frappe.workspaces[route[1]]) { + } else if (route[0] == 'private' && frappe.workspaces[private_wspace]) { // private workspace - route = ['Workspaces', 'private', frappe.workspaces[route[1]].title]; + route = ['Workspaces', 'private', frappe.workspaces[private_wspace].title]; } else if (this.routes[route[0]]) { // route route = this.set_doctype_route(route); @@ -357,7 +365,8 @@ frappe.router = { return a; } }).join('/'); - let default_page = frappe.workspaces['home'] ? 'home' : Object.keys(frappe.workspaces)[0]; + let private_home = frappe.workspaces[`home-${frappe.user.name.toLowerCase()}`]; + let default_page = private_home ? 'private/home' : frappe.workspaces['home'] ? 'home' : Object.keys(frappe.workspaces)[0]; return '/app/' + (path_string || default_page); }, diff --git a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js index 952fd62aa1..6d1d7228e3 100644 --- a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js +++ b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js @@ -50,38 +50,31 @@ frappe.search.AwesomeBar = class AwesomeBar { this.awesomplete = awesomplete; - $input.on("input", function(e) { + $input.on("input", frappe.utils.debounce(function(e) { var value = e.target.value; var txt = value.trim().replace(/\s\s+/g, ' '); var last_space = txt.lastIndexOf(' '); me.global_results = []; - // if(txt && txt.length > 1) { - // me.global.get_awesome_bar_options(txt.toLowerCase(), me); - // } - var $this = $(this); - clearTimeout($this.data('timeout')); + me.options = []; - $this.data('timeout', setTimeout(function(){ - me.options = []; - if(txt && txt.length > 1) { - if(last_space !== -1) { - me.set_specifics(txt.slice(0,last_space), txt.slice(last_space+1)); - } - me.add_defaults(txt); - me.options = me.options.concat(me.build_options(txt)); - me.options = me.options.concat(me.global_results); - } else { - me.options = me.options.concat( - me.deduplicate(frappe.search.utils.get_recent_pages(txt || ""))); - me.options = me.options.concat(frappe.search.utils.get_frequent_links()); + if (txt && txt.length > 1) { + if (last_space !== -1) { + me.set_specifics(txt.slice(0, last_space), txt.slice(last_space+1)); } - me.add_help(); + me.add_defaults(txt); + me.options = me.options.concat(me.build_options(txt)); + me.options = me.options.concat(me.global_results); + } else { + me.options = me.options.concat( + me.deduplicate(frappe.search.utils.get_recent_pages(txt || ""))); + me.options = me.options.concat(frappe.search.utils.get_frequent_links()); + } + me.add_help(); - awesomplete.list = me.deduplicate(me.options); - }, 100)); + awesomplete.list = me.deduplicate(me.options); - }); + }, 500)); var open_recent = function() { if (!this.autocomplete_open) { diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 831538d255..2baff996c6 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -23,6 +23,14 @@ if (!Array.prototype.uniqBy) { }); } +// Python's dict.setdefault ported for JS objects +Object.defineProperty(Object.prototype, "setDefault", { + value: function(key, default_value) { + if (!(key in this)) this[key] = default_value; + return this[key]; + } +}); + // Pluralize String.prototype.plural = function(revert) { const plural = { @@ -1049,18 +1057,20 @@ Object.assign(frappe.utils, { return duration; }, - seconds_to_duration(value, duration_options) { - let secs = value; - let total_duration = { - days: Math.floor(secs / (3600 * 24)), - hours: Math.floor(secs % (3600 * 24) / 3600), - minutes: Math.floor(secs % 3600 / 60), - seconds: Math.floor(secs % 60) + seconds_to_duration(seconds, duration_options) { + const round = seconds > 0 ? Math.floor : Math.ceil; + const total_duration = { + days: round(seconds / 86400), // 60 * 60 * 24 + hours: round(seconds % 86400 / 3600), + minutes: round(seconds % 3600 / 60), + seconds: round(seconds % 60) }; + if (duration_options.hide_days) { - total_duration.hours = Math.floor(secs / 3600); + total_duration.hours = round(seconds / 3600); total_duration.days = 0; } + return total_duration; }, diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 7d68919821..04cc1b9880 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -107,7 +107,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } if (this.report_name !== frappe.get_route()[1]) { - // this.toggle_loading(true); // different report this.load_report(); } @@ -518,9 +517,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } else { this.page.show_form(); } - - this.page.body[0].style.setProperty('--report-filter-height', this.page.page_form.css('height')); - this.page.body.parent().css('margin-bottom', 'unset'); } set_filters(filters) { @@ -556,6 +552,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { refresh() { this.toggle_message(true); this.toggle_report(false); + this.show_loading_screen(); let filters = this.get_filter_values(true); // only one refresh at a time @@ -645,6 +642,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { this.show_footer_message(); frappe.hide_progress(); + }).finally(() => { + this.hide_loading_screen(); }); } @@ -832,7 +831,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { if (this.raw_data.add_total_row) { data = data.slice(); data.splice(-1, 1); - this.$page.find('.layout-main-section')[0].style.setProperty('--report-total-height', '310px'); } this.$report.show(); @@ -869,6 +867,24 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } } + show_loading_screen() { + const loading_state = `
+
+ Generic Empty State +
+

${__('Loading')}...

+
`; + + this.$loading.find('div').html(loading_state); + this.$report.hide(); + this.$loading.show(); + } + + hide_loading_screen() { + this.$loading.hide(); + this.$report.show(); + } + get_chart_options(data) { let options = this.report_settings.get_chart_data ? this.report_settings.get_chart_data(data.columns, data.result) @@ -1679,6 +1695,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { .hide().appendTo(this.page.main); this.$chart = $('
').hide().appendTo(this.page.main); + + this.$loading = $(this.message_div('')).hide().appendTo(this.page.main); this.$report = $('
').appendTo(this.page.main); this.$message = $(this.message_div('')).hide().appendTo(this.page.main); } @@ -1738,11 +1756,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { this.refresh(); } - toggle_loading(flag) { - this.toggle_message(flag, __('Loading') + '...'); - } - - toggle_nothing_to_show(flag) { let message = this.prepared_report ? __('This is a background report. Please set the appropriate filters and then generate a new one.') diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index 2a92d93e30..8866a4b2af 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -50,8 +50,6 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { this.setup_columns(); super.setup_new_doc_event(); this.page.main.addClass('report-view'); - this.page.body[0].style.setProperty('--report-filter-height', this.page.page_form.css('height')); - this.page.body.parent().css('margin-bottom', 'unset'); } toggle_side_bar() { diff --git a/frappe/public/js/frappe/views/workspace/blocks/card.js b/frappe/public/js/frappe/views/workspace/blocks/card.js index 15e27fed40..9b4a2ed14f 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/card.js +++ b/frappe/public/js/frappe/views/workspace/blocks/card.js @@ -30,7 +30,7 @@ export default class Card extends Block { this.new('card', 'links'); if (this.data && this.data.card_name) { - let has_data = this.make('card', this.data.card_name, 'links'); + let has_data = this.make('card', __(this.data.card_name), 'links'); if (!has_data) return; } diff --git a/frappe/public/js/frappe/views/workspace/blocks/chart.js b/frappe/public/js/frappe/views/workspace/blocks/chart.js index e41063e6fc..02e6a66e6f 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/chart.js +++ b/frappe/public/js/frappe/views/workspace/blocks/chart.js @@ -30,7 +30,7 @@ export default class Chart extends Block { this.new('chart'); if (this.data && this.data.chart_name) { - let has_data = this.make('chart', this.data.chart_name); + let has_data = this.make('chart', __(this.data.chart_name)); if (!has_data) return; } diff --git a/frappe/public/js/frappe/views/workspace/blocks/header.js b/frappe/public/js/frappe/views/workspace/blocks/header.js index 356f9c3244..d88bc42af9 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/header.js +++ b/frappe/public/js/frappe/views/workspace/blocks/header.js @@ -27,7 +27,7 @@ export default class Header extends Block { data = {}; } - newData.text = data.text || ''; + newData.text = (data.text && __(data.text.replace(/(\n|\t)/gm, ""))) || ''; newData.level = parseInt(data.level) || this.defaultLevel.number; newData.col = parseInt(data.col) || 12; diff --git a/frappe/public/js/frappe/views/workspace/blocks/paragraph.js b/frappe/public/js/frappe/views/workspace/blocks/paragraph.js index 26afa65d51..9e5dfb68ff 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/paragraph.js +++ b/frappe/public/js/frappe/views/workspace/blocks/paragraph.js @@ -177,7 +177,7 @@ export default class Paragraph extends Block { set data(data) { this._data = data || {}; - this._element.innerHTML = this._data.text || ''; + this._element.innerHTML = __(this._data.text) || ''; } static get pasteConfig() { diff --git a/frappe/public/js/frappe/views/workspace/blocks/shortcut.js b/frappe/public/js/frappe/views/workspace/blocks/shortcut.js index f7482a06f3..96b8f47484 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/shortcut.js +++ b/frappe/public/js/frappe/views/workspace/blocks/shortcut.js @@ -29,7 +29,7 @@ export default class Shortcut extends Block { this.new('shortcut'); if (this.data && this.data.shortcut_name) { - let has_data = this.make('shortcut', this.data.shortcut_name); + let has_data = this.make('shortcut', __(this.data.shortcut_name)); if (!has_data) return; } diff --git a/frappe/public/js/frappe/views/workspace/workspace.js b/frappe/public/js/frappe/views/workspace/workspace.js index 8989814349..e6248f66cf 100644 --- a/frappe/public/js/frappe/views/workspace/workspace.js +++ b/frappe/public/js/frappe/views/workspace/workspace.js @@ -51,38 +51,37 @@ frappe.views.Workspace = class Workspace { this.body = this.wrapper.find(".layout-main-section"); } - setup_pages(reload) { - this.get_pages().then(pages => { - this.all_pages = pages.pages; - this.has_access = pages.has_access; + async setup_pages(reload) { + this.sidebar_pages = !this.discard ? await this.get_pages() : this.sidebar_pages; + this.all_pages = this.sidebar_pages.pages; + this.has_access = this.sidebar_pages.has_access; - this.all_pages.forEach(page => { - page.is_editable = !page.public || pages.has_access; - }); - - this.public_pages = this.all_pages.filter(page => page.public); - this.private_pages = this.all_pages.filter(page => !page.public); - - if (this.all_pages) { - frappe.workspaces = {}; - for (let page of this.all_pages) { - frappe.workspaces[frappe.router.slug(page.title)] = {title: page.title}; - } - if (this.new_page && this.new_page.name) { - if (!frappe.workspaces[frappe.router.slug(this.new_page.name)]) { - this.new_page = { name: this.all_pages[0].title, public: this.all_pages[0].public }; - } - if (this.new_page.public) { - frappe.set_route(`${frappe.router.slug(this.new_page.name)}`); - } else { - frappe.set_route(`private/${frappe.router.slug(this.new_page.name)}`); - } - this.new_page = null; - } - this.make_sidebar(); - reload && this.show(); - } + this.all_pages.forEach(page => { + page.is_editable = !page.public || this.has_access; }); + + this.public_pages = this.all_pages.filter(page => page.public); + this.private_pages = this.all_pages.filter(page => !page.public); + + if (this.all_pages) { + frappe.workspaces = {}; + for (let page of this.all_pages) { + frappe.workspaces[frappe.router.slug(page.name)] = {title: page.title}; + } + if (this.new_page && this.new_page.name) { + if (!frappe.workspaces[frappe.router.slug(this.new_page.label)]) { + this.new_page = { name: this.all_pages[0].title, public: this.all_pages[0].public }; + } + if (this.new_page.public) { + frappe.set_route(`${frappe.router.slug(this.new_page.name)}`); + } else { + frappe.set_route(`private/${frappe.router.slug(this.new_page.name)}`); + } + this.new_page = null; + } + this.make_sidebar(); + reload && this.show(); + } } get_pages() { @@ -95,10 +94,10 @@ frappe.views.Workspace = class Workspace { @@ -152,8 +151,8 @@ frappe.views.Workspace = class Workspace { append_item(item, container) { let is_current_page = frappe.router.slug(item.title) == frappe.router.slug(this.get_page_to_show().name) && item.public == this.get_page_to_show().public; + item.selected = is_current_page; if (is_current_page) { - item.selected = true; this.current_page = { name: item.title, public: item.public }; } @@ -219,14 +218,14 @@ frappe.views.Workspace = class Workspace { if (!this.page_data || Object.keys(this.page_data).length === 0) return; + if (this.page_data.charts && this.page_data.charts.items.length === 0) return; + return frappe.dashboard_utils.get_dashboard_settings().then(settings => { if (settings) { let chart_config = settings.chart_config ? JSON.parse(settings.chart_config) : {}; - if (this.page_data.charts && this.page_data.charts.items) { - this.page_data.charts.items.map(chart => { - chart.chart_settings = chart_config[chart.chart_name] || {}; - }); - } + this.page_data.charts.items.map(chart => { + chart.chart_settings = chart_config[chart.chart_name] || {}; + }); this.pages[page.name] = this.page_data; } }); @@ -272,8 +271,7 @@ frappe.views.Workspace = class Workspace {
`).appendTo(this.body); } - this.$page.prepend(frappe.render_template('workspace_loading_skeleton')); - this.$page.find('.codex-editor').addClass('hidden'); + this.create_skeleton(); if (this.all_pages) { let pages = page.public ? this.public_pages : this.private_pages; @@ -293,8 +291,7 @@ frappe.views.Workspace = class Workspace { this.prepare_editorjs(); $('.item-anchor').removeClass('disable-click'); - this.$page.find('.codex-editor').removeClass('hidden'); - this.$page.find('.workspace-skeleton').remove(); + this.remove_skeleton(); } } @@ -336,10 +333,10 @@ frappe.views.Workspace = class Workspace { this.page.clear_secondary_action(); this.page.clear_inner_toolbar(); - current_page.is_editable && this.page.set_secondary_action(__("Edit"), () => { + current_page.is_editable && this.page.set_secondary_action(__("Edit"), async () => { if (!this.editor || !this.editor.readOnly) return; this.is_read_only = false; - this.editor.readOnly.toggle(); + await this.editor.readOnly.toggle(); this.editor.isReady.then(() => { this.initialize_editorjs_undo(); this.setup_customization_buttons(current_page); @@ -383,13 +380,13 @@ frappe.views.Workspace = class Workspace { this.page.set_secondary_action( __("Discard"), - () => { + async () => { + this.discard = true; this.page.clear_primary_action(); this.page.clear_secondary_action(); this.page.clear_inner_toolbar(); - this.editor.readOnly.toggle(); + await this.editor.readOnly.toggle(); this.is_read_only = true; - this.deleted_sidebar_items = []; this.reload(); frappe.show_alert({ message: __("Customizations Discarded"), indicator: "info" }); } @@ -568,10 +565,10 @@ frappe.views.Workspace = class Workspace { } } ] - }).then(() => { + }).then(async () => { if (this.editor.configuration.readOnly) { this.is_read_only = false; - this.editor.readOnly.toggle(); + await this.editor.readOnly.toggle(); } this.add_page_to_sidebar(values); this.show_sidebar_actions(); @@ -646,7 +643,10 @@ frappe.views.Workspace = class Workspace { this.tools = { header: { class: this.blocks['header'], - inlineToolbar: true + inlineToolbar: true, + config: { + defaultLevel: 4 + } }, paragraph: { class: this.blocks['paragraph'], @@ -693,6 +693,7 @@ frappe.views.Workspace = class Workspace { save_page() { frappe.dom.freeze(); + this.create_skeleton(); let save = true; if (!this.title && this.current_page) { let pages = this.current_page.public ? this.public_pages : this.private_pages; @@ -740,13 +741,6 @@ frappe.views.Workspace = class Workspace { if (res.message) { me.new_page = res.message; me.pages[res.message.label] && delete me.pages[res.message.label]; - me.title = ''; - me.icon = ''; - me.parent = ''; - me.public = false; - me.sorted_public_items = []; - me.sorted_private_items = []; - me.deleted_sidebar_items = []; me.reload(); frappe.show_alert({ message: __("Page Saved Successfully"), indicator: "green" }); } @@ -759,9 +753,26 @@ frappe.views.Workspace = class Workspace { } reload() { - this.$page.prepend(frappe.render_template('workspace_loading_skeleton')); - this.$page.find('.codex-editor').addClass('hidden'); + this.title = ''; + this.icon = ''; + this.parent = ''; + this.public = false; + this.sorted_public_items = []; + this.sorted_private_items = []; + this.deleted_sidebar_items = []; + this.create_skeleton(); this.setup_pages(true); + this.discard = false; this.undo.readOnly = true; } + + create_skeleton() { + this.$page.prepend(frappe.render_template('workspace_loading_skeleton')); + this.$page.find('.codex-editor').addClass('hidden'); + } + + remove_skeleton() { + this.$page.find('.codex-editor').removeClass('hidden'); + this.$page.find('.workspace-skeleton').remove(); + } }; diff --git a/frappe/public/js/frappe/widgets/widget_group.js b/frappe/public/js/frappe/widgets/widget_group.js index d8f92edc5d..cb3fc58ea3 100644 --- a/frappe/public/js/frappe/widgets/widget_group.js +++ b/frappe/public/js/frappe/widgets/widget_group.js @@ -191,7 +191,6 @@ export class SingleWidgetGroup { Object.assign(this, opts); this.widgets_list = []; this.widgets_dict = {}; - this.widget_order = []; this.make(); } diff --git a/frappe/public/js/print_format_builder/ConfigureColumns.vue b/frappe/public/js/print_format_builder/ConfigureColumns.vue new file mode 100644 index 0000000000..da10f99e40 --- /dev/null +++ b/frappe/public/js/print_format_builder/ConfigureColumns.vue @@ -0,0 +1,111 @@ + + + diff --git a/frappe/public/js/print_format_builder/Field.vue b/frappe/public/js/print_format_builder/Field.vue new file mode 100644 index 0000000000..ca53402083 --- /dev/null +++ b/frappe/public/js/print_format_builder/Field.vue @@ -0,0 +1,360 @@ + + + diff --git a/frappe/public/js/print_format_builder/HTMLEditor.vue b/frappe/public/js/print_format_builder/HTMLEditor.vue new file mode 100644 index 0000000000..17024da503 --- /dev/null +++ b/frappe/public/js/print_format_builder/HTMLEditor.vue @@ -0,0 +1,68 @@ + + + diff --git a/frappe/public/js/print_format_builder/LetterHeadEditor.vue b/frappe/public/js/print_format_builder/LetterHeadEditor.vue new file mode 100644 index 0000000000..1eae56f81a --- /dev/null +++ b/frappe/public/js/print_format_builder/LetterHeadEditor.vue @@ -0,0 +1,341 @@ + + + diff --git a/frappe/public/js/print_format_builder/Preview.vue b/frappe/public/js/print_format_builder/Preview.vue new file mode 100644 index 0000000000..35105dee6c --- /dev/null +++ b/frappe/public/js/print_format_builder/Preview.vue @@ -0,0 +1,132 @@ + + + diff --git a/frappe/public/js/print_format_builder/PrintFormat.vue b/frappe/public/js/print_format_builder/PrintFormat.vue new file mode 100644 index 0000000000..1857bae47e --- /dev/null +++ b/frappe/public/js/print_format_builder/PrintFormat.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/frappe/public/js/print_format_builder/PrintFormatBuilder.vue b/frappe/public/js/print_format_builder/PrintFormatBuilder.vue new file mode 100644 index 0000000000..bcc3f8300f --- /dev/null +++ b/frappe/public/js/print_format_builder/PrintFormatBuilder.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/frappe/public/js/print_format_builder/PrintFormatControls.vue b/frappe/public/js/print_format_builder/PrintFormatControls.vue new file mode 100644 index 0000000000..2eefc22409 --- /dev/null +++ b/frappe/public/js/print_format_builder/PrintFormatControls.vue @@ -0,0 +1,336 @@ + + + + + diff --git a/frappe/public/js/print_format_builder/PrintFormatSection.vue b/frappe/public/js/print_format_builder/PrintFormatSection.vue new file mode 100644 index 0000000000..9a065e5e26 --- /dev/null +++ b/frappe/public/js/print_format_builder/PrintFormatSection.vue @@ -0,0 +1,245 @@ + + + + + diff --git a/frappe/public/js/print_format_builder/print_format_builder.bundle.js b/frappe/public/js/print_format_builder/print_format_builder.bundle.js new file mode 100644 index 0000000000..b2d3372daf --- /dev/null +++ b/frappe/public/js/print_format_builder/print_format_builder.bundle.js @@ -0,0 +1,64 @@ +import PrintFormatBuilderComponent from "./PrintFormatBuilder.vue"; +import { getStore } from "./store"; + +class PrintFormatBuilder { + constructor({ wrapper, page, print_format }) { + this.$wrapper = $(wrapper); + this.page = page; + this.print_format = print_format; + + this.page.clear_actions(); + this.page.clear_icons(); + this.page.clear_custom_actions(); + + this.page.set_title(__("Editing {0}", [this.print_format])); + this.page.set_primary_action(__("Save"), () => { + this.$component.$store.save_changes(); + }); + let $toggle_preview_btn = this.page.add_button( + __("Show Preview"), + () => { + this.$component.toggle_preview(); + } + ); + this.page.add_button(__("Reset Changes"), () => + this.$component.$store.reset_changes() + ); + this.page.add_menu_item(__("Edit Print Format"), () => { + frappe.set_route("Form", "Print Format", this.print_format); + }); + this.page.add_menu_item(__("Change Print Format"), () => { + frappe.set_route("print-format-builder-beta"); + }); + + let $vm = new Vue({ + el: this.$wrapper.get(0), + render: h => + h(PrintFormatBuilderComponent, { + props: { + print_format_name: print_format + } + }) + }); + this.$component = $vm.$children[0]; + let store = getStore(print_format); + store.$watch("dirty", value => { + if (value) { + this.page.set_indicator("Not Saved", "orange"); + $toggle_preview_btn.hide(); + } else { + this.page.clear_indicator(); + $toggle_preview_btn.show(); + } + }); + this.$component.$watch("show_preview", value => { + $toggle_preview_btn.text( + value ? __("Hide Preview") : __("Show Preview") + ); + }); + } +} + +frappe.provide("frappe.ui"); +frappe.ui.PrintFormatBuilder = PrintFormatBuilder; +export default PrintFormatBuilder; diff --git a/frappe/public/js/print_format_builder/store.js b/frappe/public/js/print_format_builder/store.js new file mode 100644 index 0000000000..f531a4a7e0 --- /dev/null +++ b/frappe/public/js/print_format_builder/store.js @@ -0,0 +1,177 @@ +import { create_default_layout, pluck } from "./utils"; + +let stores = {}; + +export function getStore(print_format_name) { + if (stores[print_format_name]) { + return stores[print_format_name]; + } + + let options = { + data() { + return { + print_format_name, + letterhead_name: null, + print_format: null, + letterhead: null, + doctype: null, + meta: null, + layout: null, + dirty: false, + edit_letterhead: false + }; + }, + watch: { + layout: { + deep: true, + handler() { + this.dirty = true; + } + }, + print_format: { + deep: true, + handler() { + this.dirty = true; + } + } + }, + methods: { + fetch() { + return new Promise(resolve => { + frappe.model.clear_doc( + "Print Format", + this.print_format_name + ); + frappe.model.with_doc( + "Print Format", + this.print_format_name, + () => { + let print_format = frappe.get_doc( + "Print Format", + this.print_format_name + ); + frappe.model.with_doctype( + print_format.doc_type, + () => { + this.meta = frappe.get_meta( + print_format.doc_type + ); + this.print_format = print_format; + this.layout = this.get_layout(); + this.$nextTick(() => (this.dirty = false)); + this.edit_letterhead = false; + resolve(); + } + ); + } + ); + }); + }, + update({ fieldname, value }) { + this.$set(this.print_format, fieldname, value); + }, + save_changes() { + frappe.dom.freeze(__("Saving...")); + + this.layout.sections = this.layout.sections + .filter(section => !section.remove) + .map(section => { + section.columns = section.columns.map(column => { + column.fields = column.fields + .filter(df => !df.remove) + .map(df => { + if (df.table_columns) { + df.table_columns = df.table_columns.map( + tf => { + return pluck(tf, [ + "label", + "fieldname", + "fieldtype", + "options", + "width", + "field_template" + ]); + } + ); + } + return pluck(df, [ + "label", + "fieldname", + "fieldtype", + "options", + "table_columns", + "html", + "field_template" + ]); + }); + return column; + }); + return section; + }); + + this.print_format.format_data = JSON.stringify(this.layout); + + frappe + .call("frappe.client.save", { + doc: this.print_format + }) + .then(() => { + if (this.letterhead && this.letterhead._dirty) { + return frappe + .call("frappe.client.save", { + doc: this.letterhead + }) + .then(r => (this.letterhead = r.message)); + } + }) + .then(() => this.fetch()) + .always(() => { + frappe.dom.unfreeze(); + this.$emit("after_save"); + }); + }, + reset_changes() { + this.fetch(); + }, + get_layout() { + if (this.print_format) { + if (typeof this.print_format.format_data == "string") { + return JSON.parse(this.print_format.format_data); + } + return this.print_format.format_data; + } + return null; + }, + get_default_layout() { + return create_default_layout(this.meta, this.print_format); + }, + change_letterhead(letterhead) { + return frappe.db + .get_doc("Letter Head", letterhead) + .then(doc => { + this.letterhead = doc; + }); + } + } + }; + stores[print_format_name] = new Vue(options); + return stores[print_format_name]; +} + +export let storeMixin = { + inject: ["$store"], + computed: { + print_format() { + return this.$store.print_format; + }, + layout() { + return this.$store.layout; + }, + letterhead() { + return this.$store.letterhead; + }, + meta() { + return this.$store.meta; + } + } +}; diff --git a/frappe/public/js/print_format_builder/utils.js b/frappe/public/js/print_format_builder/utils.js new file mode 100644 index 0000000000..879fe9efd2 --- /dev/null +++ b/frappe/public/js/print_format_builder/utils.js @@ -0,0 +1,159 @@ +export function create_default_layout(meta, print_format) { + let layout = { + header: get_default_header(meta), + sections: [] + }; + + let section = null, + column = null; + + function set_column(df) { + if (!section) { + set_section(); + } + column = get_new_column(df); + section.columns.push(column); + } + + function set_section(df) { + section = get_new_section(df); + column = null; + layout.sections.push(section); + } + + function get_new_section(df) { + if (!df) { + df = { label: "" }; + } + return { + label: df.label || "", + columns: [] + }; + } + + function get_new_column(df) { + if (!df) { + df = { label: "" }; + } + return { + label: df.label || "", + fields: [] + }; + } + + for (let df of meta.fields) { + if (df.fieldname) { + // make a copy to avoid mutation bugs + df = JSON.parse(JSON.stringify(df)); + } else { + continue; + } + + if (df.fieldtype === "Section Break") { + set_section(df); + } else if (df.fieldtype === "Column Break") { + set_column(df); + } else if (df.label) { + if (!column) set_column(); + + if (!df.print_hide) { + let field = { + label: df.label, + fieldname: df.fieldname, + fieldtype: df.fieldtype, + options: df.options + }; + + let field_template = get_field_template( + print_format, + df.fieldname + ); + if (field_template) { + field.label = `${__(df.label)} (${__("Field Template")})`; + field.fieldtype = "Field Template"; + field.field_template = field_template.name; + field.fieldname = df.fieldname = "_template"; + } + + if (df.fieldtype === "Table") { + field.table_columns = get_table_columns(df); + } + + column.fields.push(field); + section.has_fields = true; + } + } + } + + // remove empty sections + layout.sections = layout.sections.filter(section => section.has_fields); + + return layout; +} + +export function get_table_columns(df) { + let table_columns = []; + let table_fields = frappe.get_meta(df.options).fields; + let total_width = 0; + for (let tf of table_fields) { + if ( + !in_list(["Section Break", "Column Break"], tf.fieldtype) && + !tf.print_hide && + df.label && + total_width < 100 + ) { + let width = + typeof tf.width == "number" && tf.width < 100 + ? tf.width + : tf.width + ? 20 + : 10; + table_columns.push({ + label: tf.label, + fieldname: tf.fieldname, + fieldtype: tf.fieldtype, + options: tf.options, + width + }); + total_width += width; + } + } + return table_columns; +} + +function get_field_template(print_format, fieldname) { + let templates = print_format.__onload.print_templates || {}; + for (let template of templates) { + if (template.field === fieldname) { + return template; + } + } + return null; +} + +function get_default_header(meta) { + return `
+

${meta.name}

+

{{ doc.name }}

+
`; +} + +export function pluck(object, keys) { + let out = {}; + for (let key of keys) { + if (key in object) { + out[key] = object[key]; + } + } + return out; +} + +export function get_image_dimensions(src) { + return new Promise(resolve => { + let img = new Image(); + img.onload = function() { + resolve({ width: this.width, height: this.height }); + }; + img.src = src; + }); +} diff --git a/frappe/public/scss/common/controls.scss b/frappe/public/scss/common/controls.scss index a10cd454a6..954916c911 100644 --- a/frappe/public/scss/common/controls.scss +++ b/frappe/public/scss/common/controls.scss @@ -231,6 +231,13 @@ textarea.form-control { background-color: var(--control-bg); border-radius: var(--border-radius); padding: var(--padding-md); + + svg > rect { + fill: var(--control-bg) !important; + } + svg > g { + fill: var(--text-color) !important; + } } @media (min-width: 768px) { diff --git a/frappe/public/scss/desk/list.scss b/frappe/public/scss/desk/list.scss index 4456acabb3..a49b5a463e 100644 --- a/frappe/public/scss/desk/list.scss +++ b/frappe/public/scss/desk/list.scss @@ -147,7 +147,6 @@ .list-row-head { @extend .list-row; - padding: 15px; cursor: default; .list-subject { @@ -214,6 +213,10 @@ input.list-check-all, input.list-row-checkbox { --checkbox-right-margin: calc(var(--checkbox-size) / 2 + #{$level-margin-right}); } +input.list-check-all { + margin-left: 15px; +} + .render-list-checkbox { margin-left: 15px; } diff --git a/frappe/public/scss/desk/print_preview.scss b/frappe/public/scss/desk/print_preview.scss index 3c0acc68b8..468b37fe5a 100644 --- a/frappe/public/scss/desk/print_preview.scss +++ b/frappe/public/scss/desk/print_preview.scss @@ -14,6 +14,11 @@ } } +.preview-beta-wrapper { + border-radius: var(--border-radius); + overflow: hidden; +} + .print-toolbar { margin: 0px; padding: var(--padding-md) 0; diff --git a/frappe/public/scss/desk/report.scss b/frappe/public/scss/desk/report.scss index 2389a4f8f6..f8666602ff 100644 --- a/frappe/public/scss/desk/report.scss +++ b/frappe/public/scss/desk/report.scss @@ -84,39 +84,17 @@ margin-bottom: 10px; } -.layout-main-section { - --report-filter-height: 0px; - --report-total-height: 275px; -} - .report-wrapper { overflow: auto; - - .datatable { - height: calc(100vh - var(--report-filter-height) - 205px); - - .dt-scrollable { - height: calc(100vh - var(--report-filter-height) - var(--report-total-height)); - } - } } .report-view { .result { - min-height: 50vh !important; .dt-row:last-child:not(.dt-row-filter) { .dt-cell { border-bottom: 1px solid var(--border-color); } } - - .datatable { - height: calc(100vh - var(--report-filter-height) - 225px); - - .dt-scrollable { - height: calc(100vh - var(--report-filter-height) - 295px); - } - } } } diff --git a/frappe/public/scss/print_format.bundle.scss b/frappe/public/scss/print_format.bundle.scss new file mode 100644 index 0000000000..b01e669d71 --- /dev/null +++ b/frappe/public/scss/print_format.bundle.scss @@ -0,0 +1,5 @@ +@import "./desk/variables.scss"; +@import "./common/mixins.scss"; +@import "./common/global.scss"; +@import "./common/icons.scss"; +@import "~bootstrap/scss/bootstrap"; diff --git a/frappe/search/full_text_search.py b/frappe/search/full_text_search.py index 560ad55bf3..1d4f3fef32 100644 --- a/frappe/search/full_text_search.py +++ b/frappe/search/full_text_search.py @@ -7,7 +7,7 @@ from frappe.utils import update_progress_bar from whoosh.index import create_in, open_dir, EmptyIndexError from whoosh.fields import TEXT, ID, Schema from whoosh.qparser import MultifieldParser, FieldsPlugin, WildcardPlugin -from whoosh.query import Prefix +from whoosh.query import Prefix, FuzzyTerm from whoosh.writing import AsyncWriter @@ -23,6 +23,9 @@ class FullTextSearch: def get_schema(self): return Schema(name=ID(stored=True), content=TEXT(stored=True)) + def get_fields_to_search(self): + return ["name", "content"] + def get_id(self): return "name" @@ -120,8 +123,15 @@ class FullTextSearch: results = None out = [] + search_fields = self.get_fields_to_search() + fieldboosts = {} + + # apply reducing boost on fields based on order. 1.0, 0.5, 0.33 and so on + for idx, field in enumerate(search_fields, start=1): + fieldboosts[field] = 1.0 / idx + with ix.searcher() as searcher: - parser = MultifieldParser(["title", "content"], ix.schema) + parser = MultifieldParser(search_fields, ix.schema, termclass=FuzzyTermExtended, fieldboosts=fieldboosts) parser.remove_plugin_class(FieldsPlugin) parser.remove_plugin_class(WildcardPlugin) query = parser.parse(text) @@ -136,5 +146,13 @@ class FullTextSearch: return out + +class FuzzyTermExtended(FuzzyTerm): + def __init__(self, fieldname, text, boost=1.0, maxdist=2, prefixlength=1, + constantscore=True): + super().__init__(fieldname, text, boost=boost, maxdist=maxdist, + prefixlength=prefixlength, constantscore=constantscore) + + def get_index_path(index_name): return frappe.get_site_path("indexes", index_name) diff --git a/frappe/search/test_full_text_search.py b/frappe/search/test_full_text_search.py index 348a0ec72a..0dbc7e775b 100644 --- a/frappe/search/test_full_text_search.py +++ b/frappe/search/test_full_text_search.py @@ -125,4 +125,4 @@ def get_documents(): deploy business applications with Rich Admin Interface. CommonSearchTerm""" }) - return docs \ No newline at end of file + return docs diff --git a/frappe/search/website_search.py b/frappe/search/website_search.py index 0bc06d1a9b..30eadae6f1 100644 --- a/frappe/search/website_search.py +++ b/frappe/search/website_search.py @@ -21,6 +21,9 @@ class WebsiteSearch(FullTextSearch): title=TEXT(stored=True), path=ID(stored=True), content=TEXT(stored=True) ) + def get_fields_to_search(self): + return ["title", "content"] + def get_id(self): return "path" diff --git a/frappe/sessions.py b/frappe/sessions.py index ce104968ad..9a0f19df80 100644 --- a/frappe/sessions.py +++ b/frappe/sessions.py @@ -16,9 +16,11 @@ import frappe.translate import redis from urllib.parse import unquote from frappe.cache_manager import clear_user_cache +from frappe.query_builder import Order, DocType -@frappe.whitelist(allow_guest=True) -def clear(user=None): + +@frappe.whitelist() +def clear(): frappe.local.session_obj.update(force=True) frappe.local.db.commit() clear_user_cache(frappe.session.user) @@ -61,18 +63,14 @@ def get_sessions_to_clear(user=None, keep_current=False, device=None): simultaneous_sessions = frappe.db.get_value('User', user, 'simultaneous_sessions') or 1 offset = simultaneous_sessions - 1 - condition = '' + session = DocType("Sessions") + session_id = frappe.qb.from_(session).where((session.user == user) & (session.device.isin(device))) if keep_current: - condition = ' AND sid != {0}'.format(frappe.db.escape(frappe.session.sid)) + session_id = session_id.where(session.sid != frappe.db.escape(frappe.session.sid)) - return frappe.db.sql_list(""" - SELECT `sid` FROM `tabSessions` - WHERE `tabSessions`.user=%(user)s - AND device in %(device)s - {condition} - ORDER BY `lastupdate` DESC - LIMIT 100 OFFSET {offset}""".format(condition=condition, offset=offset), - {"user": user, "device": device}) + query = session_id.select(session.sid).offset(offset).limit(100).orderby(session.lastupdate, order=Order.desc) + + return query.run(pluck=True) def delete_session(sid=None, user=None, reason="Session Expired"): from frappe.core.doctype.activity_log.feed import logout_feed @@ -80,7 +78,10 @@ def delete_session(sid=None, user=None, reason="Session Expired"): frappe.cache().hdel("session", sid) frappe.cache().hdel("last_db_session_update", sid) if sid and not user: - user_details = frappe.db.sql("""select user from tabSessions where sid=%s""", sid, as_dict=True) + table = DocType("Sessions") + user_details = frappe.qb.from_(table).where( + table.sid == sid + ).select(table.user).run(as_dict=True) if user_details: user = user_details[0].get("user") logout_feed(user, reason) @@ -91,7 +92,7 @@ def clear_all_sessions(reason=None): """This effectively logs out all users""" frappe.only_for("Administrator") if not reason: reason = "Deleted All Active Session" - for sid in frappe.db.sql_list("select sid from `tabSessions`"): + for sid in frappe.qb.from_("Sessions").select("sid").run(pluck=True): delete_session(sid, reason=reason) def get_expired_sessions(): @@ -159,6 +160,10 @@ def get(): return bootinfo +@frappe.whitelist() +def get_boot_assets_json(): + return get_assets_json() + def get_csrf_token(): if not frappe.local.session.data.csrf_token: generate_csrf_token() diff --git a/frappe/share.py b/frappe/share.py index 030feea8fa..9b33198c9b 100644 --- a/frappe/share.py +++ b/frappe/share.py @@ -128,8 +128,11 @@ def get_shared_doctypes(user=None): """Return list of doctypes in which documents are shared for the given user.""" if not user: user = frappe.session.user - - return frappe.db.sql_list("select distinct share_doctype from tabDocShare where (user=%s or everyone=1)", user) + table = frappe.qb.DocType("DocShare") + query = frappe.qb.from_(table).where( + (table.user == user) | (table.everyone == 1) + ).select(table.share_doctype).distinct() + return query.run(pluck=True) def get_share_name(doctype, name, user, everyone): if cint(everyone): diff --git a/frappe/templates/print_format/macros.html b/frappe/templates/print_format/macros.html new file mode 100644 index 0000000000..ace992e88d --- /dev/null +++ b/frappe/templates/print_format/macros.html @@ -0,0 +1,13 @@ +{% macro render_field(df, doc) %} +{%- set value = doc.get(df.fieldname) -%} +{% include ['templates/print_format/macros/' + df.renderer + '.html', 'templates/print_format/macros/Data.html'] ignore missing %} +{% endmacro %} + +{% macro field_attributes(df) %} +{%- if df.fieldname -%} +data-fieldname="{{ df.fieldname }}" +{%- endif %} +{% if df.fieldtype -%} +data-fieldtype="{{ df.fieldtype }}" +{%- endif -%} +{% endmacro %} diff --git a/frappe/templates/print_format/macros/Attach.html b/frappe/templates/print_format/macros/Attach.html new file mode 100644 index 0000000000..523d9e057a --- /dev/null +++ b/frappe/templates/print_format/macros/Attach.html @@ -0,0 +1,7 @@ +{% extends "templates/print_format/macros/Data.html" %} + +{%- block value -%} + +{%- endblock -%} diff --git a/frappe/templates/print_format/macros/AttachImage.html b/frappe/templates/print_format/macros/AttachImage.html new file mode 100644 index 0000000000..796662f67a --- /dev/null +++ b/frappe/templates/print_format/macros/AttachImage.html @@ -0,0 +1,7 @@ +{% extends "templates/print_format/macros/Data.html" %} + +{%- block value -%} +
+ {{ df.label }} +
+{%- endblock -%} diff --git a/frappe/templates/print_format/macros/Check.html b/frappe/templates/print_format/macros/Check.html new file mode 100644 index 0000000000..fbc43608a5 --- /dev/null +++ b/frappe/templates/print_format/macros/Check.html @@ -0,0 +1,9 @@ +{% extends "templates/print_format/macros/Data.html" %} + +{%- block value -%} +
+ + + +
+{%- endblock -%} diff --git a/frappe/templates/print_format/macros/Code.html b/frappe/templates/print_format/macros/Code.html new file mode 100644 index 0000000000..e83457808a --- /dev/null +++ b/frappe/templates/print_format/macros/Code.html @@ -0,0 +1,7 @@ +{% extends "templates/print_format/macros/Data.html" %} + +{%- block value -%} +
+
{{ value }}
+
+{%- endblock -%} diff --git a/frappe/templates/print_format/macros/Color.html b/frappe/templates/print_format/macros/Color.html new file mode 100644 index 0000000000..ef7a2226c6 --- /dev/null +++ b/frappe/templates/print_format/macros/Color.html @@ -0,0 +1,8 @@ +{% extends "templates/print_format/macros/Data.html" %} + +{%- block value -%} +
+
+ {{ value }} +
+{%- endblock -%} diff --git a/frappe/templates/print_format/macros/Data.html b/frappe/templates/print_format/macros/Data.html new file mode 100644 index 0000000000..722c42ce1a --- /dev/null +++ b/frappe/templates/print_format/macros/Data.html @@ -0,0 +1,10 @@ +{% if value %} +
+ {%- block label -%} +
{{ df.label }}
+ {%- endblock -%} + {%- block value -%} +
{{ doc.get_formatted(df.fieldname) }}
+ {%- endblock -%} +
+{% endif %} diff --git a/frappe/templates/print_format/macros/Divider.html b/frappe/templates/print_format/macros/Divider.html new file mode 100644 index 0000000000..49fdf3f547 --- /dev/null +++ b/frappe/templates/print_format/macros/Divider.html @@ -0,0 +1,2 @@ +
+
diff --git a/frappe/templates/print_format/macros/FieldTemplate.html b/frappe/templates/print_format/macros/FieldTemplate.html new file mode 100644 index 0000000000..9ea7fabb22 --- /dev/null +++ b/frappe/templates/print_format/macros/FieldTemplate.html @@ -0,0 +1,4 @@ +
+ {% set template = frappe.db.get_value('Print Format Field Template', df.field_template, ['template', 'template_file', 'standard'], as_dict=1) %} + {{ frappe.render_template(template.template_file if template.standard else template.template, {'doc': doc}) }} +
diff --git a/frappe/templates/print_format/macros/HTML.html b/frappe/templates/print_format/macros/HTML.html new file mode 100644 index 0000000000..6bd3659902 --- /dev/null +++ b/frappe/templates/print_format/macros/HTML.html @@ -0,0 +1,3 @@ +
+ {{ frappe.render_template(df.html, {'doc': doc}) }} +
diff --git a/frappe/templates/print_format/macros/Markdown.html b/frappe/templates/print_format/macros/Markdown.html new file mode 100644 index 0000000000..b692283fa0 --- /dev/null +++ b/frappe/templates/print_format/macros/Markdown.html @@ -0,0 +1,9 @@ +{% extends "templates/print_format/macros/Data.html" %} + +{%- block value -%} +
+ {{ frappe.utils.md_to_html(doc.get(df.fieldname)) }} +
+{%- endblock -%} + + diff --git a/frappe/templates/print_format/macros/Rating.html b/frappe/templates/print_format/macros/Rating.html new file mode 100644 index 0000000000..2e001fb58f --- /dev/null +++ b/frappe/templates/print_format/macros/Rating.html @@ -0,0 +1,22 @@ +{% extends "templates/print_format/macros/Data.html" %} + +{% macro star(is_active=false) %} + + {%- set color = '#f6c35e' if is_active else '#dce0e3' -%} + + +{% endmacro %} + +{%- block value -%} +
+ {%- for i in range(value) -%} + {{ star(true) }} + {%- endfor -%} + {%- for i in range(5 - value) -%} + {{ star() }} + {%- endfor -%} +
+{%- endblock -%} diff --git a/frappe/templates/print_format/macros/Signature.html b/frappe/templates/print_format/macros/Signature.html new file mode 100644 index 0000000000..128ff2a927 --- /dev/null +++ b/frappe/templates/print_format/macros/Signature.html @@ -0,0 +1,7 @@ +{% extends "templates/print_format/macros/Data.html" %} + +{%- block value -%} +
+ {{ df.label }} +
+{%- endblock -%} diff --git a/frappe/templates/print_format/macros/Spacer.html b/frappe/templates/print_format/macros/Spacer.html new file mode 100644 index 0000000000..1a7336e17b --- /dev/null +++ b/frappe/templates/print_format/macros/Spacer.html @@ -0,0 +1,2 @@ +
+
diff --git a/frappe/templates/print_format/macros/Table.html b/frappe/templates/print_format/macros/Table.html new file mode 100644 index 0000000000..27c0be961c --- /dev/null +++ b/frappe/templates/print_format/macros/Table.html @@ -0,0 +1,30 @@ +{% if doc.get(df.fieldname) %} +
+
+ {{ df.label }} +
+ + {% set columns = df.table_columns %} + + + {% for column in columns %} + + {% endfor %} + + + + {% for row in doc.get(df.fieldname) %} + + {% for column in columns %} + + {% endfor %} + + {% endfor %} + +
+ {{ column.label }} +
+ {{ row.get_formatted(column.fieldname) }} +
+
+{% endif %} diff --git a/frappe/templates/print_format/print_footer.html b/frappe/templates/print_format/print_footer.html new file mode 100644 index 0000000000..bd64c0b1b2 --- /dev/null +++ b/frappe/templates/print_format/print_footer.html @@ -0,0 +1,24 @@ + +
+ {%- if layout.footer -%} + {{ frappe.render_template(layout.footer, {'doc': doc}) }} + {%- endif -%} + + {%- if letterhead -%} + {{ frappe.render_template(letterhead.footer, {'doc': doc}) }} + {%- endif -%} +
diff --git a/frappe/templates/print_format/print_format.css b/frappe/templates/print_format/print_format.css new file mode 100644 index 0000000000..480cd19439 --- /dev/null +++ b/frappe/templates/print_format/print_format.css @@ -0,0 +1,131 @@ +{% include "templates/print_format/print_format_font.css" %} + +{% macro render_margin_text(position, content) %} +@{{ position.replace('_', '-') }} { + content: {{ content }} +} +{% endmacro %} + +@page { + size: {{ print_settings.pdf_page_size or 'A4' }} portrait; + margin-top: {{ print_format.margin_top | int }}mm; + margin-bottom: {{ print_format.margin_bottom | int }}mm; + margin-left: {{ print_format.margin_left | int }}mm; + margin-right: {{ print_format.margin_right | int }}mm; + padding-top: {{ (header_height or 0) + 8 }}px; + padding-bottom: {{ (footer_height or 0) + 8 }}px; + + /* page number */ + {% set page_number_position = print_format.page_number.lower().replace(' ', '_') %} + {% if page_number_position in ['top_left', 'top_center', 'top_right', 'bottom_left', 'bottom_center', 'bottom_right'] %} + {{ render_margin_text(page_number_position, 'counter(page) " of " counter(pages)') }} + {% endif %} +} + +html, body { + font-size: {{ print_format.font_size }}px; +} + +body { + min-width: {{ body_width | int }}mm !important; + max-width: {{ body_width | int }}mm !important; +} + +/* CSS rules to fix bootstrap column rendering in PDF + https://github.com/Kozea/WeasyPrint/issues/697#issuecomment-542338732 +*/ +@media print { + .col, *[class^="col-"] { + max-width: none !important; + } +} + +@media screen { + html { + background-color: var(--gray-200); + } + body { + background-color: white; + box-shadow: var(--shadow-md); + margin: 2rem auto; + min-height: 297mm; + height: min-content; + min-width: {{ body_width | int }}mm !important; + max-width: {{ body_width | int }}mm !important; + padding-top: {{ print_format.margin_top | int }}mm; + padding-right: {{ print_format.margin_right | int }}mm; + padding-left: {{ print_format.margin_left | int }}mm; + padding-bottom: {{ print_format.margin_bottom | int }}mm; + } +} + +.section:not(:first-child) { + margin-top: 1rem; +} + +.section-label { + font-size: 1.2rem; + font-weight: 600; +} + +.field + .field { + margin-top: 0.5rem; +} + +.field .label { + font-weight: bold; +} + +.field.left-right { + display: flex; +} + +.field.left-right .label { + width: 50%; +} + +.field.left-right .value { + width: 50%; +} + +.child-table [data-fieldtype="Currency"] { + text-align: right; +} + +.table-row { + page-break-inside: avoid; +} + +.page-break { + page-break-after: always; +} + +.document-header { + border-bottom-width: 1px; + border-bottom-style: solid; + border-bottom-color: var(--gray-300); +} + +.field[data-fieldtype="Rating"] .rating-star { + width: 1.5rem; +} + +.field[data-fieldtype="Long Text"] .value, .field[data-fieldtype="Text"] .value { + white-space: pre-line; +} + +.field[data-fieldtype="Color"] .value { + display: flex; + align-items: center; +} + +.field[data-fieldtype="Color"] .color-square { + width: 1rem; + height: 1rem; + margin-right: 0.3rem; + border-radius: var(--border-radius); +} + +.field[data-fieldtype="Check"] #icon-tick { + width: 1rem; +} diff --git a/frappe/templates/print_format/print_format.html b/frappe/templates/print_format/print_format.html new file mode 100644 index 0000000000..b9fb95a9d3 --- /dev/null +++ b/frappe/templates/print_format/print_format.html @@ -0,0 +1,40 @@ +{% import "templates/print_format/macros.html" as macros %} + + + + + + + + {{ doc.doctype }}: {{ doc.name }} + {{ include_style('print_format.bundle.css') }} + + {%- if print_style and print_style.css -%} + + {%- endif -%} + {%- if print_format.css -%} + + {%- endif -%} + + + {{ header or '' }} + {% for section in layout.sections %} +
+ {% if section.label %} + + {% endif %} + +
+ {% for column in section.columns %} +
+ {% for df in column.fields %} + {{ macros.render_field(df, doc) }} + {% endfor %} +
+ {% endfor %} +
+
+ {% endfor %} + {{ footer or '' }} + + diff --git a/frappe/templates/print_format/print_format_font.css b/frappe/templates/print_format/print_format_font.css new file mode 100644 index 0000000000..6103dbf5a4 --- /dev/null +++ b/frappe/templates/print_format/print_format_font.css @@ -0,0 +1,9 @@ +@charset "UTF-8"; +{% if print_format.font %} +{% set font_family = print_format.font.replace(' ', '+') %} +@import url("https://fonts.googleapis.com/css?family={{ font_family }}:400,500,600,700"); +{% endif %} + +html, body { + font-family: {{ print_format.font or 'Inter' }}, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; +} diff --git a/frappe/templates/print_format/print_header.html b/frappe/templates/print_format/print_header.html new file mode 100644 index 0000000000..9b1357e08c --- /dev/null +++ b/frappe/templates/print_format/print_header.html @@ -0,0 +1,24 @@ + +
+ {%- if letterhead -%} + {{ frappe.render_template(letterhead.content, {'doc': doc}) }} + {%- endif -%} + + {%- if layout.header -%} + {{ frappe.render_template(layout.header, {'doc': doc}) }} + {%- endif -%} +
diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index 0d32a72756..978e3f4f7f 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -11,6 +11,7 @@ import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.utils import random_string from frappe.utils.testutils import clear_custom_fields +from frappe.query_builder import Field from .test_query_builder import run_only_if, db_type_is @@ -24,6 +25,7 @@ class TestDB(unittest.TestCase): self.assertEqual(frappe.db.get_value("User", {"name": ["<=", "Administrator"]}), "Administrator") self.assertEqual(frappe.db.get_value("User", {}, ["Max(name)"]), frappe.db.sql("SELECT Max(name) FROM tabUser")[0][0]) self.assertEqual(frappe.db.get_value("User", {}, "Min(name)"), frappe.db.sql("SELECT Min(name) FROM tabUser")[0][0]) + self.assertIn("for update", frappe.db.get_value("User", Field("name") == "Administrator", for_update=True, run=False).lower()) self.assertEqual(frappe.db.sql("""SELECT name FROM `tabUser` WHERE name > 's' ORDER BY MODIFIED DESC""")[0][0], frappe.db.get_value("User", {"name": [">", "s"]})) diff --git a/frappe/tests/test_global_search.py b/frappe/tests/test_global_search.py index 41d6427b77..9a86baa4e5 100644 --- a/frappe/tests/test_global_search.py +++ b/frappe/tests/test_global_search.py @@ -88,13 +88,13 @@ class TestGlobalSearch(unittest.TestCase): event = frappe.get_doc('Event', event_name) test_subject = event.subject results = global_search.search(test_subject) - self.assertEqual(len(results), 1) + self.assertTrue(any(r["name"] == event_name for r in results), msg="Failed to search document by exact name") frappe.delete_doc('Event', event_name) global_search.sync_global_search() results = global_search.search(test_subject) - self.assertEqual(len(results), 0) + self.assertTrue(all(r["name"] != event_name for r in results), msg="Deleted documents appearing in global search.") def test_insert_child_table(self): frappe.db.delete("Event") diff --git a/frappe/tests/test_permissions.py b/frappe/tests/test_permissions.py index 48510f55f6..f4276b2d59 100644 --- a/frappe/tests/test_permissions.py +++ b/frappe/tests/test_permissions.py @@ -493,6 +493,34 @@ class TestPermissions(unittest.TestCase): frappe.set_user("test2@example.com") self.assertRaises(frappe.PermissionError, getdoc, 'Blog Post', doc.name) + def test_if_owner_permission_on_get_list(self): + doc = frappe.get_doc({ + "doctype": "Blog Post", + "blog_category": "-test-blog-category", + "blogger": "_Test Blogger 1", + "title": "_Test If Owner Permissions on Get List", + "content": "_Test Blog Post Content" + }) + + doc.insert(ignore_if_duplicate=True) + + update('Blog Post', 'Blogger', 0, 'if_owner', 1) + update('Blog Post', 'Blogger', 0, 'read', 1) + user = frappe.get_doc("User", "test2@example.com") + user.add_roles("Website Manager") + frappe.clear_cache(doctype="Blog Post") + + frappe.set_user("test2@example.com") + self.assertIn(doc.name, frappe.get_list("Blog Post", pluck="name")) + + # Become system manager to remove role + frappe.set_user("test1@example.com") + user.remove_roles("Website Manager") + frappe.clear_cache(doctype="Blog Post") + + frappe.set_user("test2@example.com") + self.assertNotIn(doc.name, frappe.get_list("Blog Post", pluck="name")) + def test_if_owner_permission_on_delete(self): update('Blog Post', 'Blogger', 0, 'if_owner', 1) update('Blog Post', 'Blogger', 0, 'read', 1) diff --git a/frappe/tests/test_website.py b/frappe/tests/test_website.py index 818dc8bce6..25aa7b31ce 100644 --- a/frappe/tests/test_website.py +++ b/frappe/tests/test_website.py @@ -5,12 +5,12 @@ from frappe.utils import set_request from frappe.website.serve import get_response, get_response_content from frappe.website.utils import (build_response, clear_website_cache, get_home_page) - class TestWebsite(unittest.TestCase): def setUp(self): frappe.set_user('Guest') def tearDown(self): + frappe.db.value_cache = {} frappe.set_user('Administrator') def test_home_page(self): @@ -197,7 +197,7 @@ class TestWebsite(unittest.TestCase): frappe.cache().delete_key('app_hooks') def test_printview_page(self): - content = get_response_content('/Language/en') + content = get_response_content('/Language/ru') self.assertIn('
- +
+ + + +
@@ -38,7 +44,7 @@