Merge branch 'develop' of github.com:frappe/frappe into mask-sql-errors

This commit is contained in:
Gavin D'souza 2021-10-26 15:37:03 +05:30
commit c755df29bd
146 changed files with 4065 additions and 1364 deletions

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -1,146 +0,0 @@
# This file specifies rules for correctness according to how frappe doctype data model works.
rules:
- id: frappe-modifying-but-not-comitting
patterns:
- pattern: |
def $METHOD(self, ...):
...
self.$ATTR = ...
- pattern-not: |
def $METHOD(self, ...):
...
self.$ATTR = ...
...
self.db_set(..., self.$ATTR, ...)
- pattern-not: |
def $METHOD(self, ...):
...
self.$ATTR = $SOME_VAR
...
self.db_set(..., $SOME_VAR, ...)
- pattern-not: |
def $METHOD(self, ...):
...
self.$ATTR = $SOME_VAR
...
self.save()
- metavariable-regex:
metavariable: '$ATTR'
# this is negative look-ahead, add more attrs to ignore like (ignore|ignore_this_too|ignore_me)
regex: '^(?!ignore_linked_doctypes|status_updater)(.*)$'
- metavariable-regex:
metavariable: "$METHOD"
regex: "(on_submit|on_cancel)"
message: |
DocType modified in self.$METHOD. Please check if modification of self.$ATTR is commited to database.
languages: [python]
severity: ERROR
- id: frappe-modifying-but-not-comitting-other-method
patterns:
- pattern: |
class $DOCTYPE(...):
def $METHOD(self, ...):
...
self.$ANOTHER_METHOD()
...
def $ANOTHER_METHOD(self, ...):
...
self.$ATTR = ...
- pattern-not: |
class $DOCTYPE(...):
def $METHOD(self, ...):
...
self.$ANOTHER_METHOD()
...
def $ANOTHER_METHOD(self, ...):
...
self.$ATTR = ...
...
self.db_set(..., self.$ATTR, ...)
- pattern-not: |
class $DOCTYPE(...):
def $METHOD(self, ...):
...
self.$ANOTHER_METHOD()
...
def $ANOTHER_METHOD(self, ...):
...
self.$ATTR = $SOME_VAR
...
self.db_set(..., $SOME_VAR, ...)
- pattern-not: |
class $DOCTYPE(...):
def $METHOD(self, ...):
...
self.$ANOTHER_METHOD()
...
self.save()
def $ANOTHER_METHOD(self, ...):
...
self.$ATTR = ...
- metavariable-regex:
metavariable: "$METHOD"
regex: "(on_submit|on_cancel)"
message: |
self.$ANOTHER_METHOD is called from self.$METHOD, check if changes to self.$ATTR are commited to database.
languages: [python]
severity: ERROR
- id: frappe-print-function-in-doctypes
pattern: print(...)
message: |
Did you mean to leave this print statement in? Consider using msgprint or logger instead of print statement.
languages: [python]
severity: WARNING
paths:
include:
- "*/**/doctype/*"
- id: frappe-modifying-child-tables-while-iterating
pattern-either:
- pattern: |
for $ROW in self.$TABLE:
...
self.remove(...)
- pattern: |
for $ROW in self.$TABLE:
...
self.append(...)
message: |
Child table being modified while iterating on it.
languages: [python]
severity: ERROR
paths:
include:
- "*/**/doctype/*"
- id: frappe-same-key-assigned-twice
pattern-either:
- pattern: |
{..., $X: $A, ..., $X: $B, ...}
- pattern: |
dict(..., ($X, $A), ..., ($X, $B), ...)
- pattern: |
_dict(..., ($X, $A), ..., ($X, $B), ...)
message: |
key `$X` is uselessly assigned twice. This could be a potential bug.
languages: [python]
severity: ERROR
- id: frappe-using-db-sql
pattern-either:
- pattern: frappe.db.sql(...)
- pattern: frappe.db.sql_ddl(...)
- pattern: frappe.db.sql_list(...)
paths:
exclude:
- "test_*.py"
message: |
The PR contains a SQL query that may be re-written with frappe.qb (https://frappeframework.com/docs/user/en/api/query-builder) or the Database API (https://frappeframework.com/docs/user/en/api/database)
languages: [python]
severity: ERROR

View file

@ -1,6 +0,0 @@
def function_name(input):
# ruleid: frappe-codeinjection-eval
eval(input)
# ok: frappe-codeinjection-eval
eval("1 + 1")

View file

@ -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

View file

@ -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])

View file

@ -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

View file

@ -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

View file

@ -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") }}. ');

View file

@ -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"))

View file

@ -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

View file

@ -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

View file

@ -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');

View file

@ -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'
);
});
});
});

View file

@ -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');

View file

@ -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
View file

@ -0,0 +1,3 @@
Faker~=8.1.0
pyngrok~=5.0.5
unittest-xml-reporting~=3.0.4

View file

@ -46,7 +46,7 @@ let argv = yargs
})
.option("live-reload", {
type: "boolean",
description: `Automatically reload web pages when assets are rebuilt.
description: `Automatically reload Desk when assets are rebuilt.
Can only be used with the --watch flag.`
})
.option("production", {
@ -288,10 +288,24 @@ function get_watch_config() {
assets_json,
prev_assets_json
} = await write_assets_json(result.metafile);
let changed_files;
if (prev_assets_json) {
log_rebuilt_assets(prev_assets_json, assets_json);
changed_files = get_rebuilt_assets(
prev_assets_json,
assets_json
);
let timestamp = new Date().toLocaleTimeString();
let message = `${timestamp}: Compiled ${changed_files.length} files...`;
log(chalk.yellow(message));
for (let filepath of changed_files) {
let filename = path.basename(filepath);
log(" " + filename);
}
log();
}
notify_redis({ success: true });
notify_redis({ success: true, changed_files });
}
}
};
@ -461,7 +475,7 @@ function run_build_command_for_apps(apps) {
process.chdir(cwd);
}
async function notify_redis({ error, success }) {
async function notify_redis({ error, success, changed_files }) {
// notify redis which in turns tells socketio to publish this to browser
let subscriber = get_redis_subscriber("redis_socketio");
subscriber.on("error", _ => {
@ -484,6 +498,7 @@ async function notify_redis({ error, success }) {
if (success) {
payload = {
success: true,
changed_files,
live_reload: argv["live-reload"]
};
}
@ -514,7 +529,7 @@ function open_in_editor() {
subscriber.subscribe("open_in_editor");
}
function log_rebuilt_assets(prev_assets, new_assets) {
function get_rebuilt_assets(prev_assets, new_assets) {
let added_files = [];
let old_files = Object.values(prev_assets);
let new_files = Object.values(new_assets);
@ -524,17 +539,5 @@ function log_rebuilt_assets(prev_assets, new_assets) {
added_files.push(filepath);
}
}
log(
chalk.yellow(
`${new Date().toLocaleTimeString()}: Compiled ${
added_files.length
} files...`
)
);
for (let filepath of added_files) {
let filename = path.basename(filepath);
log(" " + filename);
}
log();
return added_files;
}

View file

@ -30,9 +30,6 @@ from .utils.lazy_loader import lazy_import
from frappe.query_builder import get_query_builder, patch_query_execute
# Lazy imports
faker = lazy_import('faker')
__version__ = '14.0.0-dev'
__title__ = "Frappe Framework"
@ -1838,6 +1835,7 @@ def parse_json(val):
return parse_json(val)
def mock(type, size=1, locale='en'):
import faker
results = []
fake = faker.Faker(locale)
if type not in dir(fake):

View file

@ -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": [],

View file

@ -16,7 +16,6 @@ from frappe.utils.minify import JavascriptMinify
import click
import psutil
from urllib.parse import urlparse
from simple_chalk import green
from semantic_version import Version
from requests import head
from requests.exceptions import HTTPError
@ -108,7 +107,7 @@ def fetch_assets(url, frappe_head):
if not assets_archive:
raise AssetsNotDownloadedError(f"Assets could not be retrived from {url}")
print(f"\n{green('')} Downloaded Frappe assets from {url}")
click.echo(click.style("", fg="green") + f" Downloaded Frappe assets from {url}")
return assets_archive
@ -131,7 +130,7 @@ def setup_assets(assets_archive):
directories_created.add(asset_directory)
tar.makefile(file, dest)
print("{0} Restored {1}".format(green(''), show))
click.echo(click.style("", fg="green") + f" Restored {show}")
return directories_created
@ -379,7 +378,7 @@ def make_asset_dirs(hard_link=False):
except Exception:
print(fail_message, end="\r")
print(unstrip(f"{green('')} Application Assets Linked") + "\n")
click.echo(unstrip(click.style("", fg="green") + " Application Assets Linked") + "\n")
def link_assets_dir(source, target, hard_link=False):

View file

@ -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()

View file

@ -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)})")
))

View file

@ -87,10 +87,6 @@ class DocType(Document):
if self.default_print_format and not self.custom:
frappe.throw(_('Standard DocType cannot have default print format, use Customize Form'))
if frappe.conf.get('developer_mode'):
self.owner = 'Administrator'
self.modified_by = 'Administrator'
def validate_field_name_conflicts(self):
"""Check if field names dont conflict with controller properties and methods"""
core_doctypes = [
@ -177,7 +173,6 @@ class DocType(Document):
if self.is_virtual and self.custom:
frappe.throw(_("Not allowed to create custom Virtual DocType."), CannotCreateStandardDoctypeError)
if frappe.conf.get('developer_mode'):
self.owner = 'Administrator'
self.modified_by = 'Administrator'
@ -315,9 +310,7 @@ class DocType(Document):
if allow_doctype_export:
self.export_doc()
self.make_controller_template()
if self.has_web_view:
self.set_base_class_for_controller()
self.set_base_class_for_controller()
# update index
if not self.custom:
@ -355,23 +348,49 @@ class DocType(Document):
now=now, doctype=self.name)
def set_base_class_for_controller(self):
'''Updates the controller class to subclass from `WebsiteGenertor`,
if it is a subclass of `Document`'''
controller_path = frappe.get_module_path(frappe.scrub(self.module),
'doctype', frappe.scrub(self.name), frappe.scrub(self.name) + '.py')
"""If DocType.has_web_view has been changed, updates the controller class and import
from `WebsiteGenertor` to `Document` or viceversa"""
with open(controller_path, 'r') as f:
if not self.has_value_changed("has_web_view"):
return
despaced_name = self.name.replace(" ", "_")
scrubbed_name = frappe.scrub(self.name)
scrubbed_module = frappe.scrub(self.module)
controller_path = frappe.get_module_path(
scrubbed_module, "doctype", scrubbed_name, f"{scrubbed_name}.py"
)
document_cls_tag = f"class {despaced_name}(Document)"
document_import_tag = "from frappe.model.document import Document"
website_generator_cls_tag = f"class {despaced_name}(WebsiteGenerator)"
website_generator_import_tag = "from frappe.website.generators.website_generator import WebsiteGenerator"
with open(controller_path) as f:
code = f.read()
updated_code = code
class_string = '\nclass {0}(Document)'.format(self.name.replace(' ', ''))
if '\nfrom frappe.model.document import Document' in code and class_string in code:
code = code.replace('from frappe.model.document import Document',
'from frappe.website.website_generator import WebsiteGenerator')
code = code.replace('class {0}(Document)'.format(self.name.replace(' ', '')),
'class {0}(WebsiteGenerator)'.format(self.name.replace(' ', '')))
is_website_generator_class = all([
website_generator_cls_tag in code,
website_generator_import_tag in code
])
with open(controller_path, 'w') as f:
f.write(code)
if self.has_web_view and not is_website_generator_class:
updated_code = updated_code.replace(
document_import_tag, website_generator_import_tag
).replace(
document_cls_tag, website_generator_cls_tag
)
elif not self.has_web_view and is_website_generator_class:
updated_code = updated_code.replace(
website_generator_import_tag, document_import_tag
).replace(
website_generator_cls_tag, document_cls_tag
)
if updated_code != code:
with open(controller_path, "w") as f:
f.write(updated_code)
def run_module_method(self, method):
from frappe.modules import load_doctype_module

View file

@ -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
}
}

View file

@ -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

View file

@ -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]

View file

@ -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):

View file

@ -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"""

View file

@ -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']]

View file

@ -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": [],

View file

@ -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"
}

View file

@ -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": [],

View file

@ -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)

View file

@ -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")
)

View file

@ -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": [],

View 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"
]

View file

@ -37,6 +37,7 @@ class Database(object):
STANDARD_VARCHAR_COLUMNS = ('name', 'owner', 'modified_by', 'parent', 'parentfield', 'parenttype')
DEFAULT_COLUMNS = ['name', 'creation', 'modified', 'modified_by', 'owner', 'docstatus', 'parent',
'parentfield', 'parenttype', 'idx']
MAX_WRITES_PER_TRANSACTION = 200_000
class InvalidColumnName(frappe.ValidationError): pass
@ -83,7 +84,8 @@ class Database(object):
pass
def sql(self, query, values=(), as_dict = 0, as_list = 0, formatted = 0,
debug=0, ignore_ddl=0, as_utf8=0, auto_commit=0, update=None, explain=False, run=True):
debug=0, ignore_ddl=0, as_utf8=0, auto_commit=0, update=None,
explain=False, run=True, pluck=False):
"""Execute a SQL query and fetch all rows.
:param query: SQL query.
@ -184,6 +186,9 @@ class Database(object):
if not self._cursor.description:
return ()
if pluck:
return [r[0] for r in self._cursor.fetchall()]
# scrub output if required
if as_dict:
ret = self.fetch_as_dict(formatted, as_utf8)
@ -239,7 +244,7 @@ class Database(object):
except Exception:
frappe.errprint("error in query explain")
def sql_list(self, query, values=(), debug=False):
def sql_list(self, query, values=(), debug=False, **kwargs):
"""Return data as list of single elements (first column).
Example:
@ -247,7 +252,7 @@ class Database(object):
# doctypes = ["DocType", "DocField", "User", ...]
doctypes = frappe.db.sql_list("select name from DocType")
"""
return [r[0] for r in self.sql(query, values, debug=debug)]
return [r[0] for r in self.sql(query, values, **kwargs, debug=debug)]
def sql_ddl(self, query, values=(), debug=False):
"""Commit and execute a query. DDL (Data Definition Language) queries that alter schema
@ -268,7 +273,7 @@ class Database(object):
if query[:6].lower() in ('update', 'insert', 'delete'):
self.transaction_writes += 1
if self.transaction_writes > 200000:
if self.transaction_writes > self.MAX_WRITES_PER_TRANSACTION:
if self.auto_commit_on_many_writes:
self.commit()
else:
@ -330,7 +335,7 @@ class Database(object):
return self.get_value(doctype, filters, "*", as_dict=as_dict, cache=cache)
def get_value(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
debug=False, order_by=None, cache=False, for_update=False):
debug=False, order_by=None, cache=False, for_update=False, run=True):
"""Returns a document property or list of properties.
:param doctype: DocType name.
@ -357,12 +362,15 @@ class Database(object):
"""
ret = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug,
order_by, cache=cache, for_update=for_update)
order_by, cache=cache, for_update=for_update, run=run)
if not run:
return ret
return ((len(ret[0]) > 1 or as_dict) and ret[0] or ret[0][0]) if ret else None
def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
debug=False, order_by=None, update=None, cache=False, for_update=False):
debug=False, order_by=None, update=None, cache=False, for_update=False, run=True):
"""Returns multiple document properties.
:param doctype: DocType name.
@ -388,7 +396,7 @@ class Database(object):
if isinstance(filters, list):
order_by = order_by or "modified_desc"
out = self._get_value_for_many_names(doctype, filters, fieldname, debug=debug)
out = self._get_value_for_many_names(doctype, filters, fieldname, debug=debug, run=run)
else:
fields = fieldname
@ -401,26 +409,28 @@ class Database(object):
if (filters is not None) and (filters!=doctype or doctype=="DocType"):
try:
order_by = order_by or "modified"
out = self._get_values_from_table(fields, filters, doctype, as_dict, debug, order_by, update, for_update=for_update)
out = self._get_values_from_table(
fields, filters, doctype, as_dict, debug, order_by, update, for_update=for_update, run=run
)
except Exception as e:
if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)):
# table or column not found, return None
out = None
elif (not ignore) and frappe.db.is_table_missing(e):
# table not found, look in singles
out = self.get_values_from_single(fields, filters, doctype, as_dict, debug, update)
out = self.get_values_from_single(fields, filters, doctype, as_dict, debug, update, run=run)
else:
raise
else:
out = self.get_values_from_single(fields, filters, doctype, as_dict, debug, update)
out = self.get_values_from_single(fields, filters, doctype, as_dict, debug, update, run=run)
if cache and isinstance(filters, str):
self.value_cache[(doctype, filters, fieldname)] = out
return out
def get_values_from_single(self, fields, filters, doctype, as_dict=False, debug=False, update=None):
def get_values_from_single(self, fields, filters, doctype, as_dict=False, debug=False, update=None, run=True):
"""Get values from `tabSingles` (Single DocTypes) (internal).
:param fields: List of fields,
@ -449,8 +459,9 @@ class Database(object):
r = self.sql("""select field, value
from `tabSingles` where field in (%s) and doctype=%s"""
% (', '.join(['%s'] * len(fields)), '%s'),
tuple(fields) + (doctype,), as_dict=False, debug=debug)
tuple(fields) + (doctype,), as_dict=False, debug=debug, run=run)
if not run:
return r
if as_dict:
if r:
r = frappe._dict(r)
@ -528,7 +539,8 @@ class Database(object):
"""Alias for get_single_value"""
return self.get_single_value(*args, **kwargs)
def _get_values_from_table(self, fields, filters, doctype, as_dict, debug, order_by=None, update=None, for_update=False):
def _get_values_from_table(self, fields, filters, doctype, as_dict, debug, order_by=None,
update=None, for_update=False, run=True):
field_objects = []
for field in fields:
@ -537,7 +549,9 @@ class Database(object):
else:
field_objects.append(field)
criterion = self.query.build_conditions(table=doctype, filters=filters, orderby=order_by, for_update=for_update)
criterion = self.query.build_conditions(
table=doctype, filters=filters, orderby=order_by, for_update=for_update
)
if isinstance(fields, (list, tuple)):
query = criterion.select(*field_objects)
@ -545,18 +559,17 @@ class Database(object):
if fields=="*":
query = criterion.select(fields)
as_dict = True
r = self.sql(query, as_dict=as_dict, debug=debug, update=update)
r = self.sql(query, as_dict=as_dict, debug=debug, update=update, run=run)
return r
def _get_value_for_many_names(self, doctype, names, field, debug=False):
def _get_value_for_many_names(self, doctype, names, field, debug=False, run=True):
names = list(filter(None, names))
if names:
return self.get_all(doctype,
fields=['name', field],
filters=[['name', 'in', names]],
debug=debug, as_list=1)
debug=debug, as_list=1, run=run)
else:
return {}
@ -601,7 +614,7 @@ class Database(object):
for key in to_update:
set_values.append('`{0}`=%({0})s'.format(key))
for name in self.get_values(dt, dn, 'name', for_update=for_update):
for name in self.get_values(dt, dn, 'name', for_update=for_update, debug=debug):
values = dict(name=name[0])
values.update(to_update)

View file

@ -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):

View file

@ -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:

View file

@ -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())

View file

@ -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');
}
},

View file

@ -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",

View file

@ -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()

View file

@ -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):

View file

@ -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

View file

@ -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": [],

View file

@ -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()

View file

@ -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:

View file

@ -24,7 +24,7 @@ def write_document_file(doc, record_module=None, create_init=True, folder_name=N
doc_export = doc.as_dict(no_nulls=True)
doc.run_method("before_export", doc_export)
strip_default_fields(doc, doc_export)
doc_export = strip_default_fields(doc, doc_export)
module = record_module or get_module_name(doc)
# create folder
@ -42,12 +42,17 @@ def write_document_file(doc, record_module=None, create_init=True, folder_name=N
def strip_default_fields(doc, doc_export):
# strip out default fields from children
if doc.doctype == "DocType" and doc.migration_hash:
del doc_export["migration_hash"]
for df in doc.meta.get_table_fields():
for d in doc_export.get(df.fieldname):
for fieldname in frappe.model.default_fields:
if fieldname in d:
del d[fieldname]
return doc_export
def write_code_files(folder, fname, doc, doc_export):
'''Export code files and strip from values'''
if hasattr(doc, 'get_code_fields'):
@ -59,8 +64,6 @@ def write_code_files(folder, fname, doc, doc_export):
# remove from exporting
del doc_export[key]
def get_module_name(doc):
if doc.doctype == 'Module Def':
module = doc.name

View file

@ -182,4 +182,4 @@ frappe.patches.v13_0.jinja_hook
frappe.patches.v13_0.update_notification_channel_if_empty
frappe.patches.v14_0.drop_data_import_legacy
frappe.patches.v14_0.rename_cancelled_documents
frappe.patches.v14_0.update_workspace2 # 25.08.2021
frappe.patches.v14_0.update_workspace2 # 20.09.2021

View file

@ -2,6 +2,7 @@
import frappe
from frappe.desk.form.linked_with import get_linked_doctypes
from frappe.patches.v11_0.replicate_old_user_permissions import get_doctypes_to_skip
from frappe.query_builder import Field
# `skip_for_doctype` was a un-normalized way of storing for which
# doctypes the user permission was applicable.
@ -72,16 +73,12 @@ def execute():
frappe.db.set_value('User Permission', user_permission.name, 'apply_to_all_doctypes', 1)
if new_user_permissions_list:
frappe.db.sql('''
INSERT INTO `tabUser Permission`
(`name`, `user`, `allow`, `for_value`, `applicable_for`, `apply_to_all_doctypes`, `creation`, `modified`)
VALUES {}
'''.format( # nosec
', '.join(['%s'] * len(new_user_permissions_list))
), tuple(new_user_permissions_list))
frappe.qb.into("User Permission").columns(
"name", "user", "allow", "for_value", "applicable_for", "apply_to_all_doctypes", "creation", "modified"
).insert(*new_user_permissions_list).run()
if user_permissions_to_delete:
frappe.db.sql('DELETE FROM `tabUser Permission` WHERE `name` in ({})' # nosec
.format(','.join(['%s'] * len(user_permissions_to_delete))),
tuple(user_permissions_to_delete)
frappe.db.delete(
"User Permission",
filters=(Field("name").isin(tuple(user_permissions_to_delete)))
)

View file

@ -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 = ''

View file

@ -6,7 +6,7 @@ import frappe
import frappe.share
from frappe import _, msgprint
from frappe.utils import cint
from frappe.query_builder import DocType
rights = ("select", "read", "write", "create", "delete", "submit", "cancel", "amend",
"print", "email", "report", "import", "export", "set_user_permissions", "share")
@ -330,8 +330,7 @@ def get_all_perms(role):
'''Returns valid permissions for a given role'''
perms = frappe.get_all('DocPerm', fields='*', filters=dict(role=role))
custom_perms = frappe.get_all('Custom DocPerm', fields='*', filters=dict(role=role))
doctypes_with_custom_perms = frappe.db.sql_list("""select distinct parent
from `tabCustom DocPerm`""")
doctypes_with_custom_perms = frappe.get_all("Custom DocPerm", pluck="parent", distinct=True)
for p in perms:
if p.parent not in doctypes_with_custom_perms:
@ -348,10 +347,13 @@ def get_roles(user=None, with_standard=True):
def get():
if user == 'Administrator':
return [r[0] for r in frappe.db.sql("select name from `tabRole`")] # return all available roles
return frappe.get_all("Role", pluck="name") # return all available roles
else:
return [r[0] for r in frappe.db.sql("""select role from `tabHas Role`
where parent=%s and role not in ('All', 'Guest')""", (user,))] + ['All', 'Guest']
table = DocType("Has Role")
roles = frappe.qb.from_(table).where(
(table.parent == user) & (table.role.notin(["All", "Guest"]))
).select(table.role).run(pluck=True)
return roles + ['All', 'Guest']
roles = frappe.cache().hget("roles", user, get)
@ -460,10 +462,9 @@ def update_permission_property(doctype, role, permlevel, ptype, value=None, vali
name = frappe.get_value('Custom DocPerm', dict(parent=doctype, role=role,
permlevel=permlevel))
table = DocType("Custom DocPerm")
frappe.qb.update(table).set(ptype, value).where(table.name == name).run()
frappe.db.sql("""
update `tabCustom DocPerm`
set `{0}`=%s where name=%s""".format(ptype), (value, name))
if validate:
validate_permissions_for_doctype(doctype)

View file

@ -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": [
{

View file

@ -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')

View file

@ -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) {

View file

@ -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": [
{

View file

@ -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:

View file

@ -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) {
// }
});

View file

@ -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
}

View file

@ -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)

View file

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies and Contributors
# See license.txt
# import frappe
import unittest
class TestPrintFormatFieldTemplate(unittest.TestCase):
pass

View file

@ -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' }
);
@ -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(

View file

@ -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();
}
}

View file

@ -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()

View file

@ -0,0 +1,3 @@
.layout-main-section-wrapper {
margin-bottom: 0;
}

View file

@ -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();
}
}

View file

@ -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
}

View file

@ -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))

View file

@ -7,6 +7,34 @@ 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);

View file

@ -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;
}
}

View file

@ -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";

View file

@ -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;
}
};

View file

@ -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;

View file

@ -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) {

View file

@ -320,7 +320,7 @@ export default class GridRow {
</div>
<p class='help-box small text-muted hidden-xs'>
<a class='add-new-fields text-muted'>
+ Add / Remove Columns
+ ${__('Add / Remove Columns')}
</a>
</p>
</div>
@ -397,7 +397,7 @@ export default class GridRow {
<a style='cursor: grabbing;'>${frappe.utils.icon('drag', 'xs')}</a>
</div>
<div class='col-md-7' style='padding-left:0px;'>
${docfield.label}
${__(docfield.label)}
</div>
<div class='col-md-3' style='padding-left:0px;margin-top:-2px;' title='${__('Columns')}'>
<input class='form-control column-width input-xs text-right'

View file

@ -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);
}
}

View file

@ -58,6 +58,12 @@ $('body').on('click', 'a', function(e) {
if (frappe.router.is_app_route(e.currentTarget.pathname)) {
// target has "/app, this is a v2 style route.
frappe.route_options = {};
let params = new URLSearchParams(e.currentTarget.search);
for (const [key, value] of params) {
frappe.route_options[key] = value;
}
return override(e.currentTarget.pathname + e.currentTarget.hash);
}
@ -127,12 +133,14 @@ frappe.router = {
// /app/user/user-001 = ["Form", "User", "user-001"]
// /app/event/view/calendar/default = ["List", "Event", "Calendar", "Default"]
let private_wspace = route[1] && `${route[1]}-${frappe.user.name.toLowerCase()}`;
if (frappe.workspaces[route[0]]) {
// public workspace
route = ['Workspaces', frappe.workspaces[route[0]].title];
} else if (route[0] == 'private' && frappe.workspaces[route[1]]) {
} else if (route[0] == 'private' && frappe.workspaces[private_wspace]) {
// private workspace
route = ['Workspaces', 'private', frappe.workspaces[route[1]].title];
route = ['Workspaces', 'private', frappe.workspaces[private_wspace].title];
} else if (this.routes[route[0]]) {
// route
route = this.set_doctype_route(route);
@ -357,7 +365,8 @@ frappe.router = {
return a;
}
}).join('/');
let default_page = frappe.workspaces['home'] ? 'home' : Object.keys(frappe.workspaces)[0];
let private_home = frappe.workspaces[`home-${frappe.user.name.toLowerCase()}`];
let default_page = private_home ? 'private/home' : frappe.workspaces['home'] ? 'home' : Object.keys(frappe.workspaces)[0];
return '/app/' + (path_string || default_page);
},

View file

@ -50,38 +50,31 @@ frappe.search.AwesomeBar = class AwesomeBar {
this.awesomplete = awesomplete;
$input.on("input", function(e) {
$input.on("input", frappe.utils.debounce(function(e) {
var value = e.target.value;
var txt = value.trim().replace(/\s\s+/g, ' ');
var last_space = txt.lastIndexOf(' ');
me.global_results = [];
// if(txt && txt.length > 1) {
// me.global.get_awesome_bar_options(txt.toLowerCase(), me);
// }
var $this = $(this);
clearTimeout($this.data('timeout'));
me.options = [];
$this.data('timeout', setTimeout(function(){
me.options = [];
if(txt && txt.length > 1) {
if(last_space !== -1) {
me.set_specifics(txt.slice(0,last_space), txt.slice(last_space+1));
}
me.add_defaults(txt);
me.options = me.options.concat(me.build_options(txt));
me.options = me.options.concat(me.global_results);
} else {
me.options = me.options.concat(
me.deduplicate(frappe.search.utils.get_recent_pages(txt || "")));
me.options = me.options.concat(frappe.search.utils.get_frequent_links());
if (txt && txt.length > 1) {
if (last_space !== -1) {
me.set_specifics(txt.slice(0, last_space), txt.slice(last_space+1));
}
me.add_help();
me.add_defaults(txt);
me.options = me.options.concat(me.build_options(txt));
me.options = me.options.concat(me.global_results);
} else {
me.options = me.options.concat(
me.deduplicate(frappe.search.utils.get_recent_pages(txt || "")));
me.options = me.options.concat(frappe.search.utils.get_frequent_links());
}
me.add_help();
awesomplete.list = me.deduplicate(me.options);
}, 100));
awesomplete.list = me.deduplicate(me.options);
});
}, 500));
var open_recent = function() {
if (!this.autocomplete_open) {

View file

@ -23,6 +23,14 @@ if (!Array.prototype.uniqBy) {
});
}
// Python's dict.setdefault ported for JS objects
Object.defineProperty(Object.prototype, "setDefault", {
value: function(key, default_value) {
if (!(key in this)) this[key] = default_value;
return this[key];
}
});
// Pluralize
String.prototype.plural = function(revert) {
const plural = {

View file

@ -517,9 +517,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
} else {
this.page.show_form();
}
this.page.body[0].style.setProperty('--report-filter-height', this.page.page_form.css('height'));
this.page.body.parent().css('margin-bottom', 'unset');
}
set_filters(filters) {
@ -834,7 +831,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
if (this.raw_data.add_total_row) {
data = data.slice();
data.splice(-1, 1);
this.$page.find('.layout-main-section')[0].style.setProperty('--report-total-height', '310px');
}
this.$report.show();

View file

@ -50,8 +50,6 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
this.setup_columns();
super.setup_new_doc_event();
this.page.main.addClass('report-view');
this.page.body[0].style.setProperty('--report-filter-height', this.page.page_form.css('height'));
this.page.body.parent().css('margin-bottom', 'unset');
}
toggle_side_bar() {

View file

@ -30,7 +30,7 @@ export default class Card extends Block {
this.new('card', 'links');
if (this.data && this.data.card_name) {
let has_data = this.make('card', this.data.card_name, 'links');
let has_data = this.make('card', __(this.data.card_name), 'links');
if (!has_data) return;
}

View file

@ -30,7 +30,7 @@ export default class Chart extends Block {
this.new('chart');
if (this.data && this.data.chart_name) {
let has_data = this.make('chart', this.data.chart_name);
let has_data = this.make('chart', __(this.data.chart_name));
if (!has_data) return;
}

View file

@ -27,7 +27,7 @@ export default class Header extends Block {
data = {};
}
newData.text = data.text || '';
newData.text = (data.text && __(data.text.replace(/(\n|\t)/gm, ""))) || '';
newData.level = parseInt(data.level) || this.defaultLevel.number;
newData.col = parseInt(data.col) || 12;

View file

@ -177,7 +177,7 @@ export default class Paragraph extends Block {
set data(data) {
this._data = data || {};
this._element.innerHTML = this._data.text || '';
this._element.innerHTML = __(this._data.text) || '';
}
static get pasteConfig() {

View file

@ -29,7 +29,7 @@ export default class Shortcut extends Block {
this.new('shortcut');
if (this.data && this.data.shortcut_name) {
let has_data = this.make('shortcut', this.data.shortcut_name);
let has_data = this.make('shortcut', __(this.data.shortcut_name));
if (!has_data) return;
}

View file

@ -51,38 +51,37 @@ frappe.views.Workspace = class Workspace {
this.body = this.wrapper.find(".layout-main-section");
}
setup_pages(reload) {
this.get_pages().then(pages => {
this.all_pages = pages.pages;
this.has_access = pages.has_access;
async setup_pages(reload) {
this.sidebar_pages = !this.discard ? await this.get_pages() : this.sidebar_pages;
this.all_pages = this.sidebar_pages.pages;
this.has_access = this.sidebar_pages.has_access;
this.all_pages.forEach(page => {
page.is_editable = !page.public || pages.has_access;
});
this.public_pages = this.all_pages.filter(page => page.public);
this.private_pages = this.all_pages.filter(page => !page.public);
if (this.all_pages) {
frappe.workspaces = {};
for (let page of this.all_pages) {
frappe.workspaces[frappe.router.slug(page.title)] = {title: page.title};
}
if (this.new_page && this.new_page.name) {
if (!frappe.workspaces[frappe.router.slug(this.new_page.name)]) {
this.new_page = { name: this.all_pages[0].title, public: this.all_pages[0].public };
}
if (this.new_page.public) {
frappe.set_route(`${frappe.router.slug(this.new_page.name)}`);
} else {
frappe.set_route(`private/${frappe.router.slug(this.new_page.name)}`);
}
this.new_page = null;
}
this.make_sidebar();
reload && this.show();
}
this.all_pages.forEach(page => {
page.is_editable = !page.public || this.has_access;
});
this.public_pages = this.all_pages.filter(page => page.public);
this.private_pages = this.all_pages.filter(page => !page.public);
if (this.all_pages) {
frappe.workspaces = {};
for (let page of this.all_pages) {
frappe.workspaces[frappe.router.slug(page.name)] = {title: page.title};
}
if (this.new_page && this.new_page.name) {
if (!frappe.workspaces[frappe.router.slug(this.new_page.label)]) {
this.new_page = { name: this.all_pages[0].title, public: this.all_pages[0].public };
}
if (this.new_page.public) {
frappe.set_route(`${frappe.router.slug(this.new_page.name)}`);
} else {
frappe.set_route(`private/${frappe.router.slug(this.new_page.name)}`);
}
this.new_page = null;
}
this.make_sidebar();
reload && this.show();
}
}
get_pages() {
@ -95,10 +94,10 @@ frappe.views.Workspace = class Workspace {
<div class="desk-sidebar-item standard-sidebar-item ${item.selected ? "selected" : ""}">
<a
href="/app/${item.public ? frappe.router.slug(item.title) : 'private/'+frappe.router.slug(item.title) }"
class="item-anchor ${item.is_editable ? "" : "block-click" }" title="${item.title}"
class="item-anchor ${item.is_editable ? "" : "block-click" }" title="${__(item.title)}"
>
<span class="sidebar-item-icon" item-icon=${item.icon || "folder-normal"}>${frappe.utils.icon(item.icon || "folder-normal", "md")}</span>
<span class="sidebar-item-label">${item.title}<span>
<span class="sidebar-item-label">${__(item.title)}<span>
</a>
<div class="sidebar-item-control"></div>
</div>
@ -152,8 +151,8 @@ frappe.views.Workspace = class Workspace {
append_item(item, container) {
let is_current_page = frappe.router.slug(item.title) == frappe.router.slug(this.get_page_to_show().name)
&& item.public == this.get_page_to_show().public;
item.selected = is_current_page;
if (is_current_page) {
item.selected = true;
this.current_page = { name: item.title, public: item.public };
}
@ -219,14 +218,14 @@ frappe.views.Workspace = class Workspace {
if (!this.page_data || Object.keys(this.page_data).length === 0) return;
if (this.page_data.charts && this.page_data.charts.items.length === 0) return;
return frappe.dashboard_utils.get_dashboard_settings().then(settings => {
if (settings) {
let chart_config = settings.chart_config ? JSON.parse(settings.chart_config) : {};
if (this.page_data.charts && this.page_data.charts.items) {
this.page_data.charts.items.map(chart => {
chart.chart_settings = chart_config[chart.chart_name] || {};
});
}
this.page_data.charts.items.map(chart => {
chart.chart_settings = chart_config[chart.chart_name] || {};
});
this.pages[page.name] = this.page_data;
}
});
@ -272,8 +271,7 @@ frappe.views.Workspace = class Workspace {
<div id="editorjs" class="desk-page page-main-content"></div>
`).appendTo(this.body);
}
this.$page.prepend(frappe.render_template('workspace_loading_skeleton'));
this.$page.find('.codex-editor').addClass('hidden');
this.create_skeleton();
if (this.all_pages) {
let pages = page.public ? this.public_pages : this.private_pages;
@ -293,8 +291,7 @@ frappe.views.Workspace = class Workspace {
this.prepare_editorjs();
$('.item-anchor').removeClass('disable-click');
this.$page.find('.codex-editor').removeClass('hidden');
this.$page.find('.workspace-skeleton').remove();
this.remove_skeleton();
}
}
@ -336,10 +333,10 @@ frappe.views.Workspace = class Workspace {
this.page.clear_secondary_action();
this.page.clear_inner_toolbar();
current_page.is_editable && this.page.set_secondary_action(__("Edit"), () => {
current_page.is_editable && this.page.set_secondary_action(__("Edit"), async () => {
if (!this.editor || !this.editor.readOnly) return;
this.is_read_only = false;
this.editor.readOnly.toggle();
await this.editor.readOnly.toggle();
this.editor.isReady.then(() => {
this.initialize_editorjs_undo();
this.setup_customization_buttons(current_page);
@ -383,13 +380,13 @@ frappe.views.Workspace = class Workspace {
this.page.set_secondary_action(
__("Discard"),
() => {
async () => {
this.discard = true;
this.page.clear_primary_action();
this.page.clear_secondary_action();
this.page.clear_inner_toolbar();
this.editor.readOnly.toggle();
await this.editor.readOnly.toggle();
this.is_read_only = true;
this.deleted_sidebar_items = [];
this.reload();
frappe.show_alert({ message: __("Customizations Discarded"), indicator: "info" });
}
@ -568,10 +565,10 @@ frappe.views.Workspace = class Workspace {
}
}
]
}).then(() => {
}).then(async () => {
if (this.editor.configuration.readOnly) {
this.is_read_only = false;
this.editor.readOnly.toggle();
await this.editor.readOnly.toggle();
}
this.add_page_to_sidebar(values);
this.show_sidebar_actions();
@ -646,7 +643,10 @@ frappe.views.Workspace = class Workspace {
this.tools = {
header: {
class: this.blocks['header'],
inlineToolbar: true
inlineToolbar: true,
config: {
defaultLevel: 4
}
},
paragraph: {
class: this.blocks['paragraph'],
@ -693,6 +693,7 @@ frappe.views.Workspace = class Workspace {
save_page() {
frappe.dom.freeze();
this.create_skeleton();
let save = true;
if (!this.title && this.current_page) {
let pages = this.current_page.public ? this.public_pages : this.private_pages;
@ -740,13 +741,6 @@ frappe.views.Workspace = class Workspace {
if (res.message) {
me.new_page = res.message;
me.pages[res.message.label] && delete me.pages[res.message.label];
me.title = '';
me.icon = '';
me.parent = '';
me.public = false;
me.sorted_public_items = [];
me.sorted_private_items = [];
me.deleted_sidebar_items = [];
me.reload();
frappe.show_alert({ message: __("Page Saved Successfully"), indicator: "green" });
}
@ -759,9 +753,26 @@ frappe.views.Workspace = class Workspace {
}
reload() {
this.$page.prepend(frappe.render_template('workspace_loading_skeleton'));
this.$page.find('.codex-editor').addClass('hidden');
this.title = '';
this.icon = '';
this.parent = '';
this.public = false;
this.sorted_public_items = [];
this.sorted_private_items = [];
this.deleted_sidebar_items = [];
this.create_skeleton();
this.setup_pages(true);
this.discard = false;
this.undo.readOnly = true;
}
create_skeleton() {
this.$page.prepend(frappe.render_template('workspace_loading_skeleton'));
this.$page.find('.codex-editor').addClass('hidden');
}
remove_skeleton() {
this.$page.find('.codex-editor').removeClass('hidden');
this.$page.find('.workspace-skeleton').remove();
}
};

View file

@ -191,7 +191,6 @@ export class SingleWidgetGroup {
Object.assign(this, opts);
this.widgets_list = [];
this.widgets_dict = {};
this.widget_order = [];
this.make();
}

View file

@ -0,0 +1,111 @@
<template>
<div>
<p class="mb-3 text-muted">
{{ help_message }}
</p>
<div class="row font-weight-bold">
<div class="col-8">
{{ __("Column") }}
</div>
<div class="col-4">
{{ __("Width") }}
({{ __("Total:") }} {{ total_width }})
</div>
</div>
<draggable
:list="df.table_columns"
:animation="200"
:group="df.fieldname"
handle=".icon-drag"
>
<div
class="mt-2 row align-center column-row"
v-for="column in df.table_columns"
>
<div class="col-8">
<div class="column-label d-flex align-center">
<div class="px-2 icon-drag ml-n2">
<svg class="icon icon-xs">
<use xlink:href="#icon-drag"></use>
</svg>
</div>
<div class="mt-1 ml-1">
<input
class="input-column-label"
:class="{ 'text-danger': column.invalid_width }"
type="text"
v-model="column.label"
/>
</div>
</div>
</div>
<div class="col-4 d-flex align-items-center">
<input
type="number"
class="text-right form-control"
:class="{ 'text-danger is-invalid': column.invalid_width }"
v-model.number="column.width"
min="0"
max="100"
step="5"
/>
<button
class="ml-2 btn btn-xs btn-icon"
@click="remove_column(column)"
>
<svg class="icon icon-sm">
<use xlink:href="#icon-close"></use>
</svg>
</button>
</div>
</div>
</draggable>
</div>
</template>
<script>
import draggable from "vuedraggable";
export default {
name: "ConfigureColumns",
props: ["df"],
components: {
draggable
},
methods: {
remove_column(column) {
this.$set(
this.df,
"table_columns",
this.df.table_columns.filter(_column => _column !== column)
);
}
},
computed: {
help_message() {
// prettier-ignore
return __("Drag columns to set order. Column width is set in percentage. The total width should not be more than 100. Columns marked in red will be removed.");
},
total_width() {
return this.df.table_columns.reduce((total, tf) => total + tf.width, 0);
}
}
};
</script>
<style scoped>
.icon-drag {
cursor: grab;
}
.input-column-label {
border: 1px solid transparent;
border-radius: var(--border-radius);
font-size: var(--text-md);
}
.input-column-label:focus {
border-color: var(--border-color);
outline: none;
background-color: var(--control-bg);
}
.input-column-label::placeholder {
font-style: italic;
font-weight: normal;
}
</style>

View file

@ -0,0 +1,360 @@
<template>
<div class="field" :title="df.fieldname" @click="editing = true">
<div class="field-controls">
<div>
<div
class="custom-html"
v-if="df.fieldtype == 'HTML' && df.html"
v-html="df.html"
></div>
<div
class="custom-html"
v-if="df.fieldtype == 'Field Template'"
>
{{ df.label }}
</div>
<input
v-else-if="editing && df.fieldtype != 'HTML'"
ref="label-input"
class="label-input"
type="text"
:placeholder="__('Label')"
v-model="df.label"
@keydown.enter="editing = false"
@blur="editing = false"
/>
<span v-else-if="df.label">{{ df.label }}</span>
<i class="text-muted" v-else>
{{ __("No Label") }} ({{ df.fieldname }})
</i>
</div>
<div class="field-actions">
<button
v-if="df.fieldtype == 'HTML'"
class="btn btn-xs btn-icon"
@click="edit_html"
>
<svg class="icon icon-sm">
<use xlink:href="#icon-edit"></use>
</svg>
</button>
<button
v-if="df.fieldtype == 'Table'"
class="btn btn-xs btn-default"
@click="configure_columns"
>
Configure columns
</button>
<button
class="btn btn-xs btn-icon"
@click="$set(df, 'remove', true)"
>
<svg class="icon icon-sm">
<use xlink:href="#icon-close"></use>
</svg>
</button>
</div>
</div>
<div
v-if="df.fieldtype == 'Table'"
class="table-controls row no-gutters"
:style="{ opacity: 1 }"
>
<div
class="table-column"
:style="{ width: tf.width + '%' }"
v-for="(tf, i) in df.table_columns"
:key="tf.fieldname"
>
<div class="table-field">
{{ tf.label }}
</div>
</div>
</div>
</div>
</template>
<script>
import draggable from "vuedraggable";
import ConfigureColumnsVue from "./ConfigureColumns.vue";
import { storeMixin } from "./store";
export default {
name: "Field",
mixins: [storeMixin],
props: ["df"],
components: {
draggable
},
data() {
return {
editing: false
};
},
watch: {
editing(value) {
if (value) {
this.$nextTick(() => this.$refs["label-input"].focus());
}
},
"df.table_columns": {
deep: true,
handler() {
this.validate_table_columns();
}
}
},
methods: {
edit_html() {
let d = new frappe.ui.Dialog({
title: __("Edit HTML"),
fields: [
{
label: __("HTML"),
fieldname: "html",
fieldtype: "Code",
options: "HTML"
}
],
primary_action: ({ html }) => {
html = frappe.dom.remove_script_and_style(html);
this.$set(this.df, "html", html);
d.hide();
}
});
d.set_value("html", this.df.html);
d.show();
},
configure_columns() {
let dialog = new frappe.ui.Dialog({
title: __("Configure columns for {0}", [this.df.label]),
fields: [
{
fieldtype: "HTML",
fieldname: "columns_area"
},
{
label: "",
fieldtype: "Autocomplete",
placeholder: __("Add Column"),
fieldname: "add_column",
options: this.get_all_columns(),
onchange: () => {
let fieldname = dialog.get_value("add_column");
if (fieldname) {
let column = this.get_column_to_add(fieldname);
if (column) {
this.df.table_columns.push(column);
this.$set(
this.df,
"table_columns",
this.df.table_columns
);
dialog.set_value("add_column", "");
}
}
}
}
],
on_page_show: () => {
new Vue({
el: dialog.get_field("columns_area").$wrapper.get(0),
render: h =>
h(ConfigureColumnsVue, {
props: {
df: this.df
}
})
});
},
on_hide: () => {
this.$set(
this.df,
"table_columns",
this.df.table_columns.filter(col => !col.invalid_width)
);
}
});
dialog.show();
},
get_all_columns() {
let meta = frappe.get_meta(this.df.options);
let more_columns = [
{
label: __("Sr No."),
value: "idx"
}
];
return more_columns.concat(
meta.fields
.map(tf => {
if (frappe.model.no_value_type.includes(tf.fieldtype)) {
return;
}
return {
label: tf.label,
value: tf.fieldname
};
})
.filter(Boolean)
);
},
get_column_to_add(fieldname) {
let standard_columns = {
idx: {
label: __("Sr No."),
fieldtype: "Data",
fieldname: "idx",
width: 10
}
};
if (fieldname in standard_columns) {
return standard_columns[fieldname];
}
return {
...frappe.meta.get_docfield(this.df.options, fieldname),
width: 10
};
},
validate_table_columns() {
if (this.df.fieldtype != "Table") return;
let columns = this.df.table_columns;
let total_width = 0;
for (let column of columns) {
if (!column.width) {
column.width = 10;
}
total_width += column.width;
if (total_width > 100) {
column.invalid_width = true;
} else {
column.invalid_width = false;
}
}
}
}
};
</script>
<style>
.field {
text-align: left;
width: 100%;
background-color: var(--bg-light-gray);
border-radius: var(--border-radius);
border: 1px dashed var(--gray-400);
padding: 0.5rem 0.75rem;
font-size: var(--text-sm);
}
.field-controls {
display: flex;
justify-content: space-between;
align-items: center;
}
.field:not(:first-child) {
margin-top: 0.5rem;
}
.custom-html {
padding-right: var(--padding-xs);
word-break: break-all;
}
.label-input {
background-color: transparent;
border: none;
padding: 0;
}
.label-input:focus {
outline: none;
}
.field:focus-within {
border-style: solid;
border-color: var(--gray-600);
}
.field-actions {
flex: none;
}
.field-actions .btn {
opacity: 0;
}
.field-actions .btn-icon {
box-shadow: none;
}
.btn-icon {
padding: 2px;
}
.btn-icon:hover {
background-color: white;
}
.field:hover .btn {
opacity: 1;
}
.table-controls {
display: flex;
margin-top: 1rem;
}
.table-column {
position: relative;
}
.table-field {
text-align: left;
width: 100%;
background-color: white;
border-radius: var(--border-radius);
border: 1px dashed var(--gray-400);
padding: 0.5rem 0.75rem;
font-size: var(--text-sm);
user-select: none;
white-space: nowrap;
overflow: hidden;
}
.column-resize {
position: absolute;
right: 0;
top: 0;
width: 6px;
border-radius: 2px;
height: 80%;
background-color: var(--gray-600);
transform: translate(50%, 10%);
z-index: 999;
cursor: col-resize;
}
.column-resize-actions {
position: absolute;
top: 0;
right: 0;
height: 100%;
display: flex;
align-items: center;
padding-right: 0.25rem;
}
.column-resize-actions .btn-icon {
background: white;
}
.column-resize-actions .btn-icon:hover {
background: var(--bg-light-gray);
}
.columns-input {
padding: var(--padding-sm);
}
</style>

View file

@ -0,0 +1,68 @@
<template>
<div class="html-editor">
<div class="d-flex justify-content-end">
<button
class="btn btn-default btn-xs btn-edit"
@click="toggle_edit"
>
{{ !editing ? buttonLabel : __("Done") }}
</button>
</div>
<div v-if="!editing" v-html="value"></div>
<div v-show="editing" ref="editor"></div>
</div>
</template>
<script>
export default {
name: "HTMLEditor",
props: ["value", "button-label"],
data() {
return {
editing: false
};
},
methods: {
toggle_edit() {
if (this.editing) {
this.$emit("change", this.get_value());
this.editing = false;
return;
}
this.editing = true;
if (!this.control) {
this.control = frappe.ui.form.make_control({
parent: this.$refs.editor,
df: {
fieldname: "editor",
fieldtype: "HTML Editor",
min_lines: 10,
max_lines: 30,
change: () => {
this.$emit("change", this.get_value());
}
},
render_input: true
});
}
this.control.set_value(this.value);
},
get_value() {
return frappe.dom.remove_script_and_style(this.control.get_value());
}
}
};
</script>
<style>
.html-editor {
position: relative;
border: 1px solid var(--dark-border-color);
border-radius: var(--border-radius);
padding: 1rem;
margin-bottom: 1rem;
}
.html-editor:last-child {
margin-bottom: 0;
}
</style>

View file

@ -0,0 +1,341 @@
<template>
<div class="letterhead">
<div class="mb-4 d-flex justify-content-between">
<div class="d-flex align-items-center">
<div
v-if="letterhead && $store.edit_letterhead"
class="btn-group"
role="group"
aria-label="Align Letterhead"
>
<button
v-for="direction in ['Left', 'Center', 'Right']"
type="button"
class="btn btn-xs"
@click="letterhead.align = direction"
:class="
letterhead.align == direction
? 'btn-secondary'
: 'btn-default'
"
>
{{ direction }}
</button>
</div>
<input
class="ml-4 custom-range"
v-if="letterhead && $store.edit_letterhead"
type="range"
name="image-resize"
min="20"
:max="range_input_field === 'image_width' ? 700 : 500"
:value="letterhead[range_input_field]"
@input="
e =>
(letterhead[range_input_field] = parseFloat(
e.target.value
))
"
/>
</div>
<div>
<button
class="ml-2 btn btn-default btn-xs"
v-if="letterhead && $store.edit_letterhead"
@click="upload_image"
>
{{ __("Change Image") }}
</button>
<button
v-if="letterhead && $store.edit_letterhead"
class="ml-2 btn btn-default btn-xs btn-change-letterhead"
@click="change_letterhead"
>
{{ __("Change Letter Head") }}
</button>
<button
v-if="letterhead"
class="ml-2 btn btn-default btn-xs btn-edit"
@click="toggle_edit_letterhead"
>
{{
!$store.edit_letterhead
? __("Edit Letter Head")
: __("Done")
}}
</button>
<button
v-if="!letterhead"
class="ml-2 btn btn-default btn-xs btn-edit"
@click="create_letterhead"
>
{{ __("Create Letter Head") }}
</button>
</div>
</div>
<div
v-if="letterhead && !$store.edit_letterhead"
v-html="letterhead.content"
></div>
<!-- <div v-show="letterhead && $store.edit_letterhead" ref="editor"></div> -->
<div
class="edit-letterhead"
v-if="letterhead && $store.edit_letterhead"
:style="{
justifyContent: {
Left: 'flex-start',
Center: 'center',
Right: 'flex-end'
}[letterhead.align]
}"
>
<div class="edit-image">
<div v-if="letterhead.image">
<img
:src="letterhead.image"
:style="{
width:
range_input_field === 'image_width'
? letterhead.image_width + 'px'
: null,
height:
range_input_field === 'image_height'
? letterhead.image_height + 'px'
: null
}"
/>
</div>
<button v-else class="btn btn-default" @click="upload_image">
{{ __("Upload Image") }}
</button>
</div>
</div>
</div>
</template>
<script>
import { storeMixin } from "./store";
import { get_image_dimensions } from "./utils";
export default {
name: "LetterHeadEditor",
mixins: [storeMixin],
data() {
return {
range_input_field: null,
aspect_ratio: null
};
},
watch: {
letterhead: {
deep: true,
immediate: true,
handler(letterhead) {
if (!letterhead) return;
if (letterhead.image_width && letterhead.image_height) {
let dimension =
letterhead.image_width > letterhead.image_height
? "width"
: "height";
let dimension_value = letterhead["image_" + dimension];
letterhead.content = `
<div style="text-align: ${letterhead.align.toLowerCase()};">
<img
src="${letterhead.image}"
alt="${letterhead.name}"
${dimension}="${dimension_value}"
style="${dimension}: ${dimension_value}px;">
</div>
`;
}
}
}
},
mounted() {
if (!this.letterhead) {
frappe
.call("frappe.client.get_default", { key: "letter_head" })
.then(r => {
if (r.message) {
this.set_letterhead(r.message);
}
});
}
this.$watch(
function() {
return this.letterhead
? this.letterhead[this.range_input_field]
: null;
},
function() {
if (this.aspect_ratio === null) return;
let update_field =
this.range_input_field == "image_width"
? "image_height"
: "image_width";
this.letterhead[update_field] =
update_field == "image_width"
? this.aspect_ratio * this.letterhead.image_height
: this.letterhead.image_width / this.aspect_ratio;
}
);
},
methods: {
toggle_edit_letterhead() {
if (this.$store.edit_letterhead) {
this.$store.edit_letterhead = false;
return;
}
this.$store.edit_letterhead = true;
if (!this.control) {
this.control = frappe.ui.form.make_control({
parent: this.$refs.editor,
df: {
fieldname: "letterhead",
fieldtype: "Comment",
change: () => {
this.letterhead._dirty = true;
this.letterhead.content = this.control.get_value();
}
},
render_input: true,
only_input: true,
no_wrapper: true
});
}
this.control.set_value(this.letterhead.content);
},
change_letterhead() {
let d = new frappe.ui.Dialog({
title: __("Change Letter Head"),
fields: [
{
label: __("Letter Head"),
fieldname: "letterhead",
fieldtype: "Link",
options: "Letter Head"
}
],
primary_action: ({ letterhead }) => {
if (letterhead) {
this.set_letterhead(letterhead);
}
d.hide();
}
});
d.show();
},
upload_image() {
new frappe.ui.FileUploader({
folder: "Home/Attachments",
on_success: file_doc => {
get_image_dimensions(file_doc.file_url).then(
({ width, height }) => {
this.$set(
this.letterhead,
"image",
file_doc.file_url
);
let new_width = width;
let new_height = height;
this.aspect_ratio = width / height;
this.range_input_field =
this.aspect_ratio > 1
? "image_width"
: "image_height";
if (width > 200) {
new_width = 200;
new_height = new_width / aspect_ratio;
}
if (height > 80) {
new_height = 80;
new_width = aspect_ratio * new_height;
}
this.$set(
this.letterhead,
"image_height",
new_height
);
this.$set(
this.letterhead,
"image_width",
new_width
);
}
);
}
});
},
set_letterhead(letterhead) {
this.$store.change_letterhead(letterhead).then(() => {
get_image_dimensions(this.letterhead.image).then(
({ width, height }) => {
this.aspect_ratio = width / height;
this.range_input_field =
this.aspect_ratio > 1
? "image_width"
: "image_height";
}
);
});
},
create_letterhead() {
let d = new frappe.ui.Dialog({
title: __("Create Letter Head"),
fields: [
{
label: __("Letter Head Name"),
fieldname: "name",
fieldtype: "Data"
}
],
primary_action: ({ name }) => {
return frappe.db
.insert({
doctype: "Letter Head",
letter_head_name: name,
source: "Image"
})
.then(doc => {
d.hide();
this.$store.change_letterhead(doc.name).then(() => {
this.toggle_edit_letterhead();
});
});
}
});
d.show();
}
}
};
</script>
<style scoped>
.letterhead {
position: relative;
border: 1px solid var(--dark-border-color);
border-radius: var(--border-radius);
padding: 1rem;
margin-bottom: 1rem;
}
.edit-letterhead {
display: flex;
align-items: center;
}
.edit-image {
min-width: 40px;
min-height: 40px;
border: 1px solid var(--border-color);
}
.edit-image img {
height: 100%;
}
.edit-title {
margin-left: 1rem;
border: 1px solid transparent;
border-radius: var(--border-radius);
font-size: var(--text-md);
font-weight: 600;
}
</style>

View file

@ -0,0 +1,132 @@
<template>
<div class="h-100">
<div class="row">
<div class="col">
<div class="preview-control" ref="doc-select"></div>
</div>
<div class="col">
<div class="preview-control" ref="preview-type"></div>
</div>
<div class="col d-flex">
<a
v-if="url"
class="btn btn-default btn-sm btn-new-tab"
target="_blank"
:href="url"
>
{{ __("Open in a new tab") }}
</a>
<button
v-if="url"
class="ml-3 btn btn-default btn-sm btn-new-tab"
@click="refresh"
>
{{ __("Refresh") }}
</button>
</div>
</div>
<div v-if="url && !preview_loaded">Generating preview...</div>
<iframe
ref="iframe"
:src="url"
v-if="url"
v-show="preview_loaded"
class="preview-iframe"
@load="preview_loaded = true"
></iframe>
</div>
</template>
<script>
import { storeMixin } from "./store";
export default {
name: "Preview",
mixins: [storeMixin],
data() {
return {
type: "PDF",
docname: null,
preview_loaded: false
};
},
mounted() {
this.doc_select = frappe.ui.form.make_control({
parent: this.$refs["doc-select"],
df: {
label: __("Select {0}", [__(this.doctype)]),
fieldname: "docname",
fieldtype: "Link",
options: this.doctype,
change: () => {
this.docname = this.doc_select.get_value();
}
},
render_input: true
});
this.preview_type = frappe.ui.form.make_control({
parent: this.$refs["preview-type"],
df: {
label: __("Preview type"),
fieldname: "docname",
fieldtype: "Select",
options: ["PDF", "HTML"],
change: () => {
this.type = this.preview_type.get_value();
}
},
render_input: true
});
this.preview_type.set_value(this.type);
this.get_default_docname().then(
docname => docname && this.doc_select.set_value(docname)
);
this.$store.$on("after_save", () => {
this.refresh();
});
},
methods: {
refresh() {
this.$refs.iframe.contentWindow.location.reload();
},
get_default_docname() {
return frappe.db.get_list(this.doctype, { limit: 1 }).then(doc => {
return doc.length > 0 ? doc[0].name : null;
});
}
},
computed: {
doctype() {
return this.print_format.doc_type;
},
url() {
if (!this.docname) return null;
let params = new URLSearchParams();
params.append("doctype", this.doctype);
params.append("name", this.docname);
params.append("print_format", this.print_format.name);
if (this.$store.letterhead) {
params.append("letterhead", this.$store.letterhead.name);
}
let url =
this.type == "PDF"
? `/api/method/frappe.utils.weasyprint.download_pdf`
: "/printpreview";
return `${url}?${params.toString()}`;
}
}
};
</script>
<style scoped>
.preview-iframe {
width: 100%;
height: 96%;
border: none;
border-radius: var(--border-radius);
}
.btn-new-tab {
margin-top: auto;
margin-bottom: 1.2rem;
}
.preview-control >>> .form-control {
background: var(--control-bg-on-gray);
}
</style>

View file

@ -0,0 +1,136 @@
<template>
<div class="print-format-main" :style="rootStyles">
<div :style="page_number_style">{{ __("1 of 2") }}</div>
<LetterHeadEditor type="Header" />
<HTMLEditor
:value="layout.header"
@change="$set(layout, 'header', $event)"
:button-label="__('Edit Header')"
/>
<draggable
class="mb-4"
v-model="layout.sections"
group="sections"
filter=".section-columns, .column, .field"
:animation="200"
>
<PrintFormatSection
v-for="(section, i) in layout.sections"
:key="i"
:section="section"
@add_section_above="add_section_above(section)"
/>
</draggable>
<HTMLEditor
:value="layout.footer"
@change="$set(layout, 'footer', $event)"
:button-label="__('Edit Footer')"
/>
<HTMLEditor
v-if="letterhead"
:value="letterhead.footer"
@change="update_letterhead_footer"
:button-label="__('Edit Letter Head Footer')"
/>
</div>
</template>
<script>
import draggable from "vuedraggable";
import HTMLEditor from "./HTMLEditor.vue";
import LetterHeadEditor from "./LetterHeadEditor.vue";
import PrintFormatSection from "./PrintFormatSection.vue";
import { storeMixin } from "./store";
export default {
name: "PrintFormat",
mixins: [storeMixin],
components: {
draggable,
PrintFormatSection,
LetterHeadEditor,
HTMLEditor
},
computed: {
rootStyles() {
let {
margin_top = 0,
margin_bottom = 0,
margin_left = 0,
margin_right = 0
} = this.print_format;
return {
padding: `${margin_top}mm ${margin_right}mm ${margin_bottom}mm ${margin_left}mm`,
width: "210mm",
minHeight: "297mm"
};
},
page_number_style() {
let style = {
position: "absolute",
background: "white",
padding: "4px",
borderRadius: "var(--border-radius)",
border: "1px solid var(--border-color)"
};
if (this.print_format.page_number.includes("Top")) {
style.top = this.print_format.margin_top / 2 + "mm";
style.transform = "translateY(-50%)";
}
if (this.print_format.page_number.includes("Left")) {
style.left = this.print_format.margin_left + "mm";
}
if (this.print_format.page_number.includes("Right")) {
style.right = this.print_format.margin_right + "mm";
}
if (this.print_format.page_number.includes("Bottom")) {
style.bottom = this.print_format.margin_bottom / 2 + "mm";
style.transform = "translateY(50%)";
}
if (this.print_format.page_number.includes("Center")) {
style.left = "50%";
style.transform += " translateX(-50%)";
}
if (this.print_format.page_number.includes("Hide")) {
style.display = "none";
}
return style;
}
},
methods: {
add_section_above(section) {
let sections = [];
for (let _section of this.layout.sections) {
if (_section === section) {
sections.push({
label: "",
columns: [
{ label: "", fields: [] },
{ label: "", fields: [] }
]
});
}
sections.push(_section);
}
this.$set(this.layout, "sections", sections);
},
update_letterhead_footer(val) {
this.letterhead.footer = val;
this.letterhead._dirty = true;
}
}
};
</script>
<style scoped>
.print-format-main {
position: relative;
margin-right: auto;
margin-left: auto;
background-color: white;
box-shadow: var(--shadow-lg);
border-radius: var(--border-radius);
}
</style>

View file

@ -0,0 +1,74 @@
<template>
<div class="layout-main-section row" v-if="shouldRender">
<div class="col-3">
<PrintFormatControls />
</div>
<div class="print-format-container col-9">
<keep-alive>
<Preview v-if="show_preview" />
<PrintFormat v-else />
</keep-alive>
</div>
</div>
</template>
<script>
import PrintFormat from "./PrintFormat.vue";
import Preview from "./Preview.vue";
import PrintFormatControls from "./PrintFormatControls.vue";
import { getStore } from "./store";
export default {
name: "PrintFormatBuilder",
props: ["print_format_name"],
components: {
PrintFormat,
PrintFormatControls,
Preview
},
data() {
return {
show_preview: false
};
},
provide() {
return {
$store: this.$store
};
},
mounted() {
this.$store.fetch().then(() => {
if (!this.$store.layout) {
this.$store.layout = this.$store.get_default_layout();
this.$store.save_changes();
}
});
},
methods: {
toggle_preview() {
this.show_preview = !this.show_preview;
}
},
computed: {
$store() {
return getStore(this.print_format_name);
},
shouldRender() {
return Boolean(
this.$store.print_format &&
this.$store.meta &&
this.$store.layout
);
}
}
};
</script>
<style scoped>
.print-format-container {
height: calc(100vh - 140px);
overflow-y: auto;
padding-top: 0.5rem;
padding-bottom: 4rem;
}
</style>

Some files were not shown because too many files have changed in this diff Show more