Merge branch 'develop' into multiple_imap_folder
This commit is contained in:
commit
d8c8ca0e2f
168 changed files with 4326 additions and 1484 deletions
1
.github/helper/install.sh
vendored
1
.github/helper/install.sh
vendored
|
|
@ -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
|
||||
|
|
|
|||
38
.github/helper/semgrep_rules/README.md
vendored
38
.github/helper/semgrep_rules/README.md
vendored
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
133
.github/helper/semgrep_rules/frappe_correctness.yml
vendored
133
.github/helper/semgrep_rules/frappe_correctness.yml
vendored
|
|
@ -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
|
||||
6
.github/helper/semgrep_rules/security.py
vendored
6
.github/helper/semgrep_rules/security.py
vendored
|
|
@ -1,6 +0,0 @@
|
|||
def function_name(input):
|
||||
# ruleid: frappe-codeinjection-eval
|
||||
eval(input)
|
||||
|
||||
# ok: frappe-codeinjection-eval
|
||||
eval("1 + 1")
|
||||
25
.github/helper/semgrep_rules/security.yml
vendored
25
.github/helper/semgrep_rules/security.yml
vendored
|
|
@ -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
|
||||
44
.github/helper/semgrep_rules/translate.js
vendored
44
.github/helper/semgrep_rules/translate.js
vendored
|
|
@ -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])
|
||||
61
.github/helper/semgrep_rules/translate.py
vendored
61
.github/helper/semgrep_rules/translate.py
vendored
|
|
@ -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
|
||||
64
.github/helper/semgrep_rules/translate.yml
vendored
64
.github/helper/semgrep_rules/translate.yml
vendored
|
|
@ -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
|
||||
9
.github/helper/semgrep_rules/ux.js
vendored
9
.github/helper/semgrep_rules/ux.js
vendored
|
|
@ -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") }}. ');
|
||||
31
.github/helper/semgrep_rules/ux.py
vendored
31
.github/helper/semgrep_rules/ux.py
vendored
|
|
@ -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"))
|
||||
30
.github/helper/semgrep_rules/ux.yml
vendored
30
.github/helper/semgrep_rules/ux.yml
vendored
|
|
@ -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
|
||||
32
.github/try-on-f-cloud-button.svg
vendored
Normal file
32
.github/try-on-f-cloud-button.svg
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<svg width="201" height="60" viewBox="0 0 201 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_dd)">
|
||||
<rect x="4" y="2" width="193" height="52" rx="6" fill="#2490EF"/>
|
||||
<path d="M28 22.2891H32.8786V35.5H36.2088V22.2891H41.0874V19.5H28V22.2891Z" fill="white"/>
|
||||
<path d="M41.6982 35.5H45.0129V28.7109C45.0129 27.2344 46.0866 26.2188 47.5494 26.2188C48.0085 26.2188 48.6388 26.2969 48.95 26.3984V23.4453C48.6543 23.375 48.2419 23.3281 47.9074 23.3281C46.5691 23.3281 45.472 24.1094 45.0362 25.5938H44.9117V23.5H41.6982V35.5Z" fill="white"/>
|
||||
<path d="M52.8331 40C55.2996 40 56.6068 38.7344 57.2837 36.7969L61.9289 23.5156L58.4197 23.5L55.9221 32.3125H55.7976L53.3233 23.5H49.8374L54.1247 35.8437L53.9302 36.3516C53.4944 37.4766 52.6619 37.5312 51.4947 37.1719L50.7478 39.6562C51.2224 39.8594 51.9927 40 52.8331 40Z" fill="white"/>
|
||||
<path d="M73.6142 35.7344C77.2401 35.7344 79.4966 33.2422 79.4966 29.5469C79.4966 25.8281 77.2401 23.3438 73.6142 23.3438C69.9883 23.3438 67.7319 25.8281 67.7319 29.5469C67.7319 33.2422 69.9883 35.7344 73.6142 35.7344ZM73.6298 33.1562C71.9569 33.1562 71.101 31.6171 71.101 29.5233C71.101 27.4296 71.9569 25.8827 73.6298 25.8827C75.2715 25.8827 76.1274 27.4296 76.1274 29.5233C76.1274 31.6171 75.2715 33.1562 73.6298 33.1562Z" fill="white"/>
|
||||
<path d="M84.7253 28.5625C84.7331 27.0156 85.6512 26.1094 86.9895 26.1094C88.3201 26.1094 89.1215 26.9844 89.1137 28.4531V35.5H92.4284V27.8594C92.4284 25.0625 90.7945 23.3438 88.3046 23.3438C86.5306 23.3438 85.2466 24.2187 84.7097 25.6172H84.5697V23.5H81.4106V35.5H84.7253V28.5625Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M102.429 19.5H113.429V22.3141H102.429V19.5ZM102.429 35.5V26.6794H112.699V29.4982H105.94V35.5H102.429Z" fill="white"/>
|
||||
<path d="M131.584 24.9625C131.09 21.5057 128.345 19.5 124.785 19.5C120.589 19.5 117.429 22.463 117.429 27.4924C117.429 32.5142 120.55 35.4848 124.785 35.4848C128.604 35.4848 131.137 33.0916 131.584 30.1211L128.651 30.1059C128.282 31.9293 126.745 32.9549 124.824 32.9549C122.22 32.9549 120.354 31.0632 120.354 27.4924C120.354 23.9824 122.204 22.0299 124.832 22.0299C126.784 22.0299 128.314 23.1011 128.651 24.9625H131.584Z" fill="white"/>
|
||||
<path d="M136.409 19.7124H133.571V35.2718H136.409V19.7124Z" fill="white"/>
|
||||
<path d="M144.031 35.5001C147.56 35.5001 149.803 33.0917 149.803 29.483C149.803 25.8667 147.56 23.4507 144.031 23.4507C140.502 23.4507 138.259 25.8667 138.259 29.483C138.259 33.0917 140.502 35.5001 144.031 35.5001ZM144.047 33.2969C142.094 33.2969 141.137 31.6103 141.137 29.4754C141.137 27.3406 142.094 25.6312 144.047 25.6312C145.968 25.6312 146.925 27.3406 146.925 29.4754C146.925 31.6103 145.968 33.2969 144.047 33.2969Z" fill="white"/>
|
||||
<path d="M159.338 30.3641C159.338 32.1419 158.028 33.0232 156.773 33.0232C155.409 33.0232 154.499 32.0887 154.499 30.6072V23.6025H151.66V31.0327C151.66 33.8361 153.307 35.4239 155.675 35.4239C157.479 35.4239 158.749 34.5046 159.298 33.1979H159.424V35.272H162.176V23.6025H159.338V30.3641Z" fill="white"/>
|
||||
<path d="M169.014 35.4769C171.084 35.4769 172.017 34.2841 172.464 33.4332H172.637V35.2718H175.429V19.7124H172.582V25.532H172.464C172.033 24.6887 171.147 23.4503 169.022 23.4503C166.238 23.4503 164.05 25.5624 164.05 29.4522C164.05 33.2965 166.175 35.4769 169.014 35.4769ZM169.806 33.2205C167.931 33.2205 166.943 31.6251 166.943 29.437C166.943 27.2642 167.916 25.7067 169.806 25.7067C171.633 25.7067 172.637 27.173 172.637 29.437C172.637 31.701 171.617 33.2205 169.806 33.2205Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd" x="0" y="0" width="201" height="60" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.25"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2"/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.13 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
6
.github/workflows/semgrep.yml
vendored
6
.github/workflows/semgrep.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
<div align="center">
|
||||
<a href="https://frappecloud.com/deploy?apps=frappe&source=frappe_readme">
|
||||
<img src=".github/try-on-f-cloud-button.svg" height="40">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## 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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
carryforward: true
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
// });
|
||||
// });
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
3
dev-requirements.txt
Normal file
3
dev-requirements.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
Faker~=8.1.0
|
||||
pyngrok~=5.0.5
|
||||
unittest-xml-reporting~=3.0.4
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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": [],
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
doctype = DocType("Activity Log")
|
||||
frappe.db.delete(doctype, filters=(
|
||||
doctype.creation < PseudoColumn(f"({Now() - Interval(days=days)})")
|
||||
))
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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]))
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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('<a href="/app/List/Error%20Log/List"> Error Logs </a>')
|
||||
}
|
||||
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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']]
|
||||
|
|
|
|||
|
|
@ -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": [],
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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": [],
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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": [],
|
||||
|
|
|
|||
56
frappe/data/google_fonts.json
Normal file
56
frappe/data/google_fonts.json
Normal file
|
|
@ -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"
|
||||
]
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <b>{0}</b> 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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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": [],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)))
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 = ''
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 = '<img src="{}">'.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'''
|
||||
<div style="text-align: {self.align.lower()};">
|
||||
<img src="{self.image}" alt="{self.name}" {dimension}="{dimension_value}" style="{dimension}: {dimension_value}px;">
|
||||
</div>
|
||||
'''
|
||||
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')
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
||||
// }
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Copyright (c) 2021, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestPrintFormatFieldTemplate(unittest.TestCase):
|
||||
pass
|
||||
|
|
@ -41,7 +41,11 @@ frappe.ui.form.PrintView = class {
|
|||
</iframe>
|
||||
</div>
|
||||
<div class="page-break-message text-muted text-center text-medium margin-top"></div>
|
||||
</div>`
|
||||
</div>
|
||||
<div class="preview-beta-wrapper">
|
||||
<iframe width="100%" height="0" frameBorder="0"></iframe>
|
||||
</div>
|
||||
`
|
||||
);
|
||||
|
||||
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(`
|
||||
<a style="line-height: 2.4" href="/app/print-format-builder-beta?doctype=${this.frm.doctype}">
|
||||
${__('Try the new Print Format Builder')}
|
||||
</a>
|
||||
`);
|
||||
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
.layout-main-section-wrapper {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
@ -3,8 +3,11 @@
|
|||
v-if="is_shown"
|
||||
class="flex justify-between build-success-message align-center"
|
||||
>
|
||||
<div class="mr-4">Compiled successfully</div>
|
||||
<a class="text-white underline" href="/" @click.prevent="reload">
|
||||
Compiled successfully
|
||||
<a
|
||||
v-if="!live_reload"
|
||||
class="ml-4 text-white underline" href="/" @click.prevent="reload"
|
||||
>
|
||||
Refresh
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 = $('<div class="build-success-container">')
|
||||
.appendTo($container)
|
||||
|
|
@ -27,7 +56,7 @@ function show_build_success() {
|
|||
});
|
||||
success = vm.$children[0];
|
||||
}
|
||||
success.show();
|
||||
success.show(data);
|
||||
}
|
||||
|
||||
function show_build_error(data) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue