Merge branch 'develop' into paytm-integration
This commit is contained in:
commit
06c54f5322
512 changed files with 266144 additions and 259520 deletions
2
.github/workflows/translation_linter.yml
vendored
2
.github/workflows/translation_linter.yml
vendored
|
|
@ -18,5 +18,5 @@ jobs:
|
|||
- name: Validating Translation Syntax
|
||||
run: |
|
||||
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
|
||||
files=$(git diff --name-only $GITHUB_BASE_REF)
|
||||
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
|
||||
python $GITHUB_WORKSPACE/.github/frappe_linter/translation.py $files
|
||||
43
.snyk
43
.snyk
|
|
@ -1,5 +1,5 @@
|
|||
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
|
||||
version: v1.13.5
|
||||
version: v1.14.1
|
||||
# ignores vulnerabilities until expiry date; change duration by modifying expiry date
|
||||
ignore:
|
||||
SNYK-JS-AWESOMPLETE-174474:
|
||||
|
|
@ -22,3 +22,44 @@ patch:
|
|||
SNYK-JS-LODASH-450202:
|
||||
- frappe-datatable > lodash:
|
||||
patched: '2020-01-31T01:33:09.889Z'
|
||||
SNYK-JS-LODASH-567746:
|
||||
- frappe-datatable > lodash:
|
||||
patched: '2020-04-30T23:02:32.330Z'
|
||||
- quagga > lodash:
|
||||
patched: '2020-04-30T23:02:32.330Z'
|
||||
- snyk > lodash:
|
||||
patched: '2020-04-30T23:02:32.330Z'
|
||||
- tailwindcss > lodash:
|
||||
patched: '2020-04-30T23:02:32.330Z'
|
||||
- '@tailwindcss/ui > @tailwindcss/custom-forms > lodash':
|
||||
patched: '2020-04-30T23:02:32.330Z'
|
||||
- snyk > @snyk/dep-graph > lodash:
|
||||
patched: '2020-04-30T23:02:32.330Z'
|
||||
- snyk > inquirer > lodash:
|
||||
patched: '2020-04-30T23:02:32.330Z'
|
||||
- snyk > snyk-config > lodash:
|
||||
patched: '2020-04-30T23:02:32.330Z'
|
||||
- snyk > snyk-mvn-plugin > lodash:
|
||||
patched: '2020-04-30T23:02:32.330Z'
|
||||
- snyk > snyk-nodejs-lockfile-parser > lodash:
|
||||
patched: '2020-04-30T23:02:32.330Z'
|
||||
- snyk > snyk-nuget-plugin > lodash:
|
||||
patched: '2020-04-30T23:02:32.330Z'
|
||||
- snyk > @snyk/dep-graph > graphlib > lodash:
|
||||
patched: '2020-04-30T23:02:32.330Z'
|
||||
- snyk > snyk-go-plugin > graphlib > lodash:
|
||||
patched: '2020-04-30T23:02:32.330Z'
|
||||
- snyk > snyk-nodejs-lockfile-parser > graphlib > lodash:
|
||||
patched: '2020-04-30T23:02:32.330Z'
|
||||
- snyk > @snyk/snyk-cocoapods-plugin > @snyk/dep-graph > lodash:
|
||||
patched: '2020-04-30T23:02:32.330Z'
|
||||
- snyk > snyk-nuget-plugin > dotnet-deps-parser > lodash:
|
||||
patched: '2020-04-30T23:02:32.330Z'
|
||||
- snyk > snyk-php-plugin > @snyk/composer-lockfile-parser > lodash:
|
||||
patched: '2020-04-30T23:02:32.330Z'
|
||||
- snyk > @snyk/snyk-cocoapods-plugin > @snyk/dep-graph > graphlib > lodash:
|
||||
patched: '2020-04-30T23:02:32.330Z'
|
||||
- snyk > @snyk/snyk-cocoapods-plugin > @snyk/cocoapods-lockfile-parser > @snyk/ruby-semver > lodash:
|
||||
patched: '2020-04-30T23:02:32.330Z'
|
||||
- snyk > @snyk/snyk-cocoapods-plugin > @snyk/cocoapods-lockfile-parser > @snyk/dep-graph > graphlib > lodash:
|
||||
patched: '2020-04-30T23:02:32.330Z'
|
||||
|
|
|
|||
64
.travis.yml
64
.travis.yml
|
|
@ -1,5 +1,5 @@
|
|||
language: python
|
||||
dist: trusty
|
||||
dist: bionic
|
||||
|
||||
addons:
|
||||
hosts:
|
||||
|
|
@ -7,42 +7,66 @@ addons:
|
|||
- test_site_producer
|
||||
mariadb: 10.3
|
||||
postgresql: 9.5
|
||||
chrome: stable
|
||||
|
||||
services:
|
||||
- xvfb
|
||||
- mysql
|
||||
|
||||
git:
|
||||
depth: 1
|
||||
|
||||
cache:
|
||||
- pip
|
||||
- npm
|
||||
- yarn
|
||||
pip: true
|
||||
npm: true
|
||||
yarn: true
|
||||
directories:
|
||||
# we also need to cache folder with Cypress binary
|
||||
# https://docs.cypress.io/guides/guides/continuous-integration.html#Caching
|
||||
- ~/.cache
|
||||
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- name: "Python 3.6 MariaDB"
|
||||
python: 3.6
|
||||
- name: "Python 3.7 MariaDB"
|
||||
python: 3.7
|
||||
env: DB=mariadb TYPE=server
|
||||
script: bench --site test_site run-tests --coverage
|
||||
|
||||
- name: "Python 3.6 PostgreSQL"
|
||||
python: 3.6
|
||||
- name: "Python 3.7 PostgreSQL"
|
||||
python: 3.7
|
||||
env: DB=postgres TYPE=server
|
||||
script: bench --site test_site run-tests --coverage
|
||||
|
||||
- name: "Cypress"
|
||||
python: 3.6
|
||||
python: 3.7
|
||||
env: DB=mariadb TYPE=ui
|
||||
before_script:
|
||||
- bench --site test_site execute frappe.utils.install.complete_setup_wizard
|
||||
- bench --site test_site_producer execute frappe.utils.install.complete_setup_wizard
|
||||
script: bench --site test_site run-ui-tests frappe --headless
|
||||
|
||||
- name: "Python 2.7 MariaDB"
|
||||
python: 2.7
|
||||
env: DB=mariadb TYPE=server
|
||||
script: bench --site test_site run-tests --coverage
|
||||
|
||||
before_install:
|
||||
# install wkhtmltopdf
|
||||
# do we really want to run travis?
|
||||
- |
|
||||
ONLY_DOCS_CHANGES=$(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '\.(md|png|jpg|jpeg)$|^.github|LICENSE' ; echo $?)
|
||||
ONLY_JS_CHANGES=$(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '\.js$' ; echo $?)
|
||||
ONLY_PY_CHANGES=$(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '\.py$' ; echo $?)
|
||||
|
||||
if [[ $ONLY_DOCS_CHANGES == "1" ]]; then
|
||||
echo "Only docs were updated, stopping build process.";
|
||||
exit;
|
||||
fi
|
||||
if [[ $ONLY_JS_CHANGES == "1" && $TYPE == "server" ]]; then
|
||||
echo "Only JavaScript code was updated; Stopping Python build process.";
|
||||
exit;
|
||||
fi
|
||||
if [[ $ONLY_PY_CHANGES == "1" && $TYPE == "ui" ]]; then
|
||||
echo "Only Python code was updated, stopping Cypress build process.";
|
||||
exit;
|
||||
fi
|
||||
|
||||
# install wkhtmltopdf
|
||||
- wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
|
||||
- tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
|
||||
- sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
|
||||
|
|
@ -54,10 +78,9 @@ before_install:
|
|||
install:
|
||||
- cd ~
|
||||
- source ./.nvm/nvm.sh
|
||||
- nvm install v8.10.0
|
||||
- nvm install 12
|
||||
|
||||
- git clone https://github.com/frappe/bench --depth 1
|
||||
- pip install -e ./bench
|
||||
- pip install frappe-bench
|
||||
|
||||
- bench init frappe-bench --skip-assets --python $(which python) --frappe-path $TRAVIS_BUILD_DIR
|
||||
|
||||
|
|
@ -101,6 +124,11 @@ install:
|
|||
|
||||
- if [ $TYPE == "ui" ]; then bench setup requirements --node; fi
|
||||
|
||||
# install node-sass which is required for website theme test
|
||||
- cd ./apps/frappe
|
||||
- yarn add node-sass@4.13.1
|
||||
- cd ../..
|
||||
|
||||
- bench start &
|
||||
- bench --site test_site reinstall --yes
|
||||
- bench --site test_site_producer reinstall --yes
|
||||
|
|
|
|||
25
CODEOWNERS
25
CODEOWNERS
|
|
@ -3,16 +3,17 @@
|
|||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence,
|
||||
|
||||
* @frappe/frappe-review-team
|
||||
website/ @scmmishra
|
||||
web_form/ @scmmishra
|
||||
templates/ @scmmishra
|
||||
www/ @scmmishra
|
||||
integrations/ @Mangesh-Khairnar
|
||||
patches/ @sahil28297
|
||||
dashboard/ @prssanna
|
||||
email/ @Thunderbottom
|
||||
event_streaming/ @ruchamahabal
|
||||
data_import* @netchampfaris
|
||||
core/ @surajshetty3416
|
||||
* @frappe/frappe-review-team
|
||||
website/ @scmmishra
|
||||
web_form/ @scmmishra
|
||||
templates/ @scmmishra
|
||||
www/ @scmmishra
|
||||
integrations/ @Mangesh-Khairnar
|
||||
patches/ @sahil28297
|
||||
dashboard/ @prssanna
|
||||
email/ @Thunderbottom
|
||||
event_streaming/ @ruchamahabal
|
||||
data_import* @netchampfaris
|
||||
core/ @surajshetty3416
|
||||
requirements.txt @gavindsouza
|
||||
commands/ @gavindsouza
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@
|
|||
"baseUrl": "http://test_site_ui:8000",
|
||||
"projectId": "92odwv",
|
||||
"adminPassword": "admin",
|
||||
"defaultCommandTimeout": 10000,
|
||||
"defaultCommandTimeout": 20000,
|
||||
"pageLoadTimeout": 15000
|
||||
}
|
||||
|
|
|
|||
45
cypress/integration/control_duration.js
Normal file
45
cypress/integration/control_duration.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
context('Control Duration', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
});
|
||||
|
||||
function get_dialog_with_duration(show_days=1, show_seconds=1) {
|
||||
return cy.dialog({
|
||||
title: 'Duration',
|
||||
fields: [{
|
||||
'fieldname': 'duration',
|
||||
'fieldtype': 'Duration',
|
||||
'show_seconds': show_days,
|
||||
'show_days': show_seconds
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
it('should set duration', () => {
|
||||
get_dialog_with_duration().as('dialog');
|
||||
cy.get('.frappe-control[data-fieldname=duration] input')
|
||||
.first()
|
||||
.click();
|
||||
cy.get('.duration-input[data-duration=days]')
|
||||
.type(45, {force: true})
|
||||
.blur({force: true});
|
||||
cy.get('.duration-input[data-duration=minutes]')
|
||||
.type(30)
|
||||
.blur({force: true});
|
||||
cy.get('.frappe-control[data-fieldname=duration] input').first().should('have.value', '45d 30m');
|
||||
cy.get('.frappe-control[data-fieldname=duration] input').first().blur();
|
||||
cy.get('.duration-picker').should('not.be.visible');
|
||||
cy.get('@dialog').then(dialog => {
|
||||
let value = dialog.get_value('duration');
|
||||
expect(value).to.equal(3889800);
|
||||
});
|
||||
});
|
||||
|
||||
it('should hide days or seconds according to duration options', () => {
|
||||
get_dialog_with_duration(0, 0).as('dialog');
|
||||
cy.get('.frappe-control[data-fieldname=duration] input').first().click();
|
||||
cy.get('.duration-input[data-duration=days]').should('not.be.visible');
|
||||
cy.get('.duration-input[data-duration=seconds]').should('not.be.visible');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
context('Control Link', () => {
|
||||
beforeEach(() => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.create_records({
|
||||
doctype: 'ToDo',
|
||||
description: 'this is a test todo for link'
|
||||
|
|
@ -30,7 +34,7 @@ context('Control Link', () => {
|
|||
|
||||
cy.get('.frappe-control[data-fieldname=link] input').focus().as('input');
|
||||
cy.wait('@search_link');
|
||||
cy.get('@input').type('todo for link');
|
||||
cy.get('@input').type('todo for link', { delay: 200 });
|
||||
cy.wait('@search_link');
|
||||
cy.get('.frappe-control[data-fieldname=link] ul').should('be.visible');
|
||||
cy.get('.frappe-control[data-fieldname=link] input').type('{enter}', { delay: 100 });
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
context('Depends On', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
return cy.new_form('Test Depends On');
|
||||
});
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ context('FileUploader', () => {
|
|||
open_upload_dialog();
|
||||
|
||||
cy.get_open_dialog().find('a:contains("web link")').click();
|
||||
cy.get_open_dialog().find('.file-web-link input').type('https://github.com');
|
||||
cy.get_open_dialog().find('.file-web-link input').type('https://github.com', { delay: 100, force: true });
|
||||
cy.server();
|
||||
cy.route('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.get_open_dialog().find('.btn-primary').click();
|
||||
|
|
|
|||
|
|
@ -6,14 +6,17 @@ context('Form', () => {
|
|||
return frappe.call("frappe.tests.ui_test_helpers.create_contact_records");
|
||||
});
|
||||
});
|
||||
beforeEach(() => {
|
||||
cy.visit('/desk#workspace/Website');
|
||||
});
|
||||
it('create a new form', () => {
|
||||
cy.visit('/desk#Form/ToDo/New ToDo 1');
|
||||
cy.fill_field('description', 'this is a test todo', 'Text Editor').blur();
|
||||
cy.get('.page-title').should('contain', 'Not Saved');
|
||||
cy.server();
|
||||
cy.route({
|
||||
method: 'POST',
|
||||
url: 'api/method/frappe.desk.form.save.savedocs'
|
||||
}).as('form_save');
|
||||
cy.get('.primary-action').click();
|
||||
cy.wait('@form_save').its('status').should('eq', 200);
|
||||
cy.visit('/desk#List/ToDo');
|
||||
cy.location('hash').should('eq', '#List/ToDo/List');
|
||||
cy.get('h1').should('be.visible').and('contain', 'To Do');
|
||||
|
|
@ -41,4 +44,21 @@ context('Form', () => {
|
|||
list_view.filter_area.filter_list.clear_filters();
|
||||
});
|
||||
});
|
||||
it('validates behaviour of Data options validations in child table', () => {
|
||||
// test email validations for set_invalid controller
|
||||
let website_input = 'website.in';
|
||||
let expectBackgroundColor = 'rgb(255, 220, 220)';
|
||||
|
||||
cy.visit('/desk#Form/Contact/New Contact 1');
|
||||
cy.get('.frappe-control[data-fieldname="email_ids"]').as('table');
|
||||
cy.get('@table').find('button.grid-add-row').click();
|
||||
cy.get('.grid-body .rows [data-fieldname="email_id"]').click();
|
||||
cy.get('@table').find('input.input-with-feedback.form-control').as('email_input');
|
||||
cy.get('@email_input').type(website_input, { waitForAnimations: false });
|
||||
cy.fill_field('company_name', 'Test Company');
|
||||
cy.get('@email_input').should($div => {
|
||||
const style = window.getComputedStyle($div[0]);
|
||||
expect(style.backgroundColor).to.equal(expectBackgroundColor);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ context('List View Settings', () => {
|
|||
cy.get('.sidebar-stat').should('contain', "Tags");
|
||||
});
|
||||
it('disable count and sidebar stats then verify', () => {
|
||||
cy.wait(300);
|
||||
cy.visit('/desk#List/DocType/List');
|
||||
cy.wait(300);
|
||||
cy.get('.list-count').should('contain', "20 of");
|
||||
cy.get('button').contains('Menu').click();
|
||||
cy.get('.dropdown-menu li').filter(':visible').contains('Settings').click();
|
||||
|
|
|
|||
|
|
@ -21,6 +21,15 @@ context('Login', () => {
|
|||
cy.location('pathname').should('eq', '/login');
|
||||
});
|
||||
|
||||
it('shows invalid login if incorrect credentials', () => {
|
||||
cy.get('#login_email').type('Administrator');
|
||||
cy.get('#login_password').type('qwer');
|
||||
|
||||
cy.get('.btn-login').click();
|
||||
cy.get('.page-card-head').contains('Invalid Login. Try again.');
|
||||
cy.location('pathname').should('eq', '/login');
|
||||
});
|
||||
|
||||
it('logs in using correct credentials', () => {
|
||||
cy.get('#login_email').type('Administrator');
|
||||
cy.get('#login_password').type(Cypress.config('adminPassword'));
|
||||
|
|
@ -30,12 +39,30 @@ context('Login', () => {
|
|||
cy.window().its('frappe.session.user').should('eq', 'Administrator');
|
||||
});
|
||||
|
||||
it('shows invalid login if incorrect credentials', () => {
|
||||
it('check redirect after login', () => {
|
||||
|
||||
// mock for OAuth 2.0 client_id, redirect_uri, scope and state
|
||||
const payload = new URLSearchParams({
|
||||
uuid: '6fed1519-cfd8-4a2d-84a6-9a1799c7c741',
|
||||
encoded_string: 'hello all',
|
||||
encoded_url: 'http://test.localhost/callback',
|
||||
base64_string: 'aGVsbG8gYWxs'
|
||||
});
|
||||
|
||||
cy.request('/api/method/logout');
|
||||
|
||||
// redirect-to /me page with params to mock OAuth 2.0 like request
|
||||
cy.visit(
|
||||
'/login?redirect-to=/me?' +
|
||||
encodeURIComponent(payload.toString().replace("+", " "))
|
||||
);
|
||||
|
||||
cy.get('#login_email').type('Administrator');
|
||||
cy.get('#login_password').type('qwer');
|
||||
cy.get('#login_password').type(Cypress.config('adminPassword'));
|
||||
|
||||
cy.get('.btn-login').click();
|
||||
cy.get('.page-card-head').contains('Invalid Login. Try again.');
|
||||
cy.location('pathname').should('eq', '/login');
|
||||
|
||||
// verify redirected location and url params after login
|
||||
cy.url().should('include', '/me?' + payload.toString().replace('+', '%20'));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
context('Relative Timeframe', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
});
|
||||
before(() => {
|
||||
cy.login();
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
|
|||
if (fieldtype === 'Select') {
|
||||
cy.get('@input').select(value);
|
||||
} else {
|
||||
cy.get('@input').type(value, { waitForAnimations: false });
|
||||
cy.get('@input').type(value, { waitForAnimations: false, force: true });
|
||||
}
|
||||
return cy.get('@input');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ if PY2:
|
|||
reload(sys)
|
||||
sys.setdefaultencoding("utf-8")
|
||||
|
||||
__version__ = '12.0.0-dev'
|
||||
__version__ = '13.0.0-dev'
|
||||
__title__ = "Frappe Framework"
|
||||
|
||||
local = Local()
|
||||
|
|
@ -48,8 +48,12 @@ class _dict(dict):
|
|||
def copy(self):
|
||||
return _dict(dict(self).copy())
|
||||
|
||||
def _(msg, lang=None):
|
||||
"""Returns translated string in current lang, if exists."""
|
||||
def _(msg, lang=None, context=None):
|
||||
"""Returns translated string in current lang, if exists.
|
||||
Usage:
|
||||
_('Change')
|
||||
_('Change', context='Coins')
|
||||
"""
|
||||
from frappe.translate import get_full_dict
|
||||
from frappe.utils import strip_html_tags, is_html
|
||||
|
||||
|
|
@ -59,7 +63,7 @@ def _(msg, lang=None):
|
|||
if not lang:
|
||||
lang = local.lang
|
||||
|
||||
non_translated_msg = msg
|
||||
non_translated_string = msg
|
||||
|
||||
if is_html(msg):
|
||||
msg = strip_html_tags(msg)
|
||||
|
|
@ -67,8 +71,16 @@ def _(msg, lang=None):
|
|||
# msg should always be unicode
|
||||
msg = as_unicode(msg).strip()
|
||||
|
||||
translated_string = ''
|
||||
if context:
|
||||
string_key = '{msg}:{context}'.format(msg=msg, context=context)
|
||||
translated_string = get_full_dict(lang).get(string_key)
|
||||
|
||||
if not translated_string:
|
||||
translated_string = get_full_dict(lang).get(msg)
|
||||
|
||||
# return lang_full_dict according to lang passed parameter
|
||||
return get_full_dict(lang).get(msg) or non_translated_msg
|
||||
return translated_string or non_translated_string
|
||||
|
||||
def as_unicode(text, encoding='utf-8'):
|
||||
'''Convert to unicode if required'''
|
||||
|
|
@ -333,7 +345,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None,
|
|||
style="margin: 0;">{}</table>'''.format(table_rows)
|
||||
|
||||
if flags.print_messages and out.message:
|
||||
print("Message: " + repr(out.message).encode("utf-8"))
|
||||
print(f"Message: {repr(out.message).encode('utf-8')}")
|
||||
|
||||
if title:
|
||||
out.title = title
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ from frappe.core.doctype.comment.comment import update_comments_in_parent_after_
|
|||
from frappe import _
|
||||
import frappe.recorder
|
||||
import frappe.monitor
|
||||
import frappe.rate_limiter
|
||||
|
||||
local_manager = LocalManager([frappe.local])
|
||||
|
||||
|
|
@ -54,6 +55,7 @@ def application(request):
|
|||
|
||||
frappe.recorder.record()
|
||||
frappe.monitor.start()
|
||||
frappe.rate_limiter.apply()
|
||||
|
||||
if frappe.local.form_dict.cmd:
|
||||
response = frappe.handler.handle()
|
||||
|
|
@ -93,9 +95,13 @@ def application(request):
|
|||
if response and hasattr(frappe.local, 'cookie_manager'):
|
||||
frappe.local.cookie_manager.flush_cookies(response=response)
|
||||
|
||||
frappe.rate_limiter.update()
|
||||
frappe.monitor.stop(response)
|
||||
frappe.recorder.dump()
|
||||
|
||||
if response and hasattr(frappe.local, 'rate_limiter'):
|
||||
response.headers.extend(frappe.local.rate_limiter.headers())
|
||||
|
||||
frappe.destroy()
|
||||
|
||||
return response
|
||||
|
|
@ -171,6 +177,9 @@ def handle_exception(e):
|
|||
http_status_code=http_status_code, indicator_color='red')
|
||||
return_as_message = True
|
||||
|
||||
elif http_status_code == 429:
|
||||
response = frappe.rate_limiter.respond()
|
||||
|
||||
else:
|
||||
traceback = "<pre>" + sanitize_html(frappe.get_traceback()) + "</pre>"
|
||||
if frappe.local.flags.disable_traceback:
|
||||
|
|
|
|||
|
|
@ -219,7 +219,10 @@ class LoginManager:
|
|||
user = frappe.db.get_value("User", filters={"username": user}, fieldname="name") or user
|
||||
|
||||
self.check_if_enabled(user)
|
||||
self.user = self.check_password(user, pwd)
|
||||
if not frappe.form_dict.get('tmp_id'):
|
||||
self.user = self.check_password(user, pwd)
|
||||
else:
|
||||
self.user = user
|
||||
|
||||
def force_user_to_reset_password(self):
|
||||
if not self.user:
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ class AssignmentRule(Document):
|
|||
user = self.get_user()
|
||||
|
||||
assign_to.add(dict(
|
||||
assign_to = user,
|
||||
assign_to = [user],
|
||||
doctype = doc.get('doctype'),
|
||||
name = doc.get('name'),
|
||||
description = frappe.render_template(self.description, doc),
|
||||
|
|
|
|||
|
|
@ -299,17 +299,20 @@ def get_next_schedule_date(schedule_date, frequency, start_date, repeat_on_day=N
|
|||
next_date = get_next_date(start_date, month_count)
|
||||
else:
|
||||
days = 7 if frequency == 'Weekly' else 1
|
||||
next_date = add_days(start_date, days)
|
||||
next_date = add_days(schedule_date, days)
|
||||
|
||||
# next schedule date should be after or on current date
|
||||
if not for_full_schedule:
|
||||
while getdate(next_date) < getdate(today()):
|
||||
if month_count:
|
||||
month_count += month_map.get(frequency)
|
||||
next_date = get_next_date(start_date, month_count, day_count)
|
||||
next_date = get_next_date(start_date, month_count, day_count)
|
||||
elif days:
|
||||
next_date = add_days(next_date, days)
|
||||
|
||||
return next_date
|
||||
|
||||
|
||||
def get_next_date(dt, mcount, day=None):
|
||||
dt = getdate(dt)
|
||||
dt += relativedelta(months=mcount, day=day)
|
||||
|
|
|
|||
|
|
@ -99,13 +99,18 @@ class TestAutoRepeat(unittest.TestCase):
|
|||
def test_next_schedule_date(self):
|
||||
current_date = getdate(today())
|
||||
todo = frappe.get_doc(
|
||||
dict(doctype='ToDo', description='test next schedule date todo', assigned_by='Administrator')).insert()
|
||||
dict(doctype='ToDo', description='test next schedule date for monthly', assigned_by='Administrator')).insert()
|
||||
doc = make_auto_repeat(frequency='Monthly', reference_document=todo.name, start_date=add_months(today(), -2))
|
||||
|
||||
# next_schedule_date is set as on or after current date
|
||||
# it should not be a previous month's date
|
||||
self.assertTrue((doc.next_schedule_date >= current_date))
|
||||
|
||||
todo = frappe.get_doc(
|
||||
dict(doctype='ToDo', description='test next schedule date for daily', assigned_by='Administrator')).insert()
|
||||
doc = make_auto_repeat(frequency='Daily', reference_document=todo.name, start_date=add_days(today(), -2))
|
||||
self.assertEqual(getdate(doc.next_schedule_date), current_date)
|
||||
|
||||
|
||||
def make_auto_repeat(**args):
|
||||
args = frappe._dict(args)
|
||||
|
|
|
|||
|
|
@ -106,14 +106,22 @@ def load_desktop_data(bootinfo):
|
|||
from frappe.desk.desktop import get_desk_sidebar_items
|
||||
bootinfo.allowed_modules = get_modules_from_all_apps_for_user()
|
||||
bootinfo.allowed_workspaces = get_desk_sidebar_items(True)
|
||||
bootinfo.dashboards = frappe.get_all("Dashboard")
|
||||
|
||||
def get_allowed_pages():
|
||||
return get_user_pages_or_reports('Page')
|
||||
def get_allowed_pages(cache=False):
|
||||
return get_user_pages_or_reports('Page', cache=cache)
|
||||
|
||||
def get_allowed_reports():
|
||||
return get_user_pages_or_reports('Report')
|
||||
def get_allowed_reports(cache=False):
|
||||
return get_user_pages_or_reports('Report', cache=cache)
|
||||
|
||||
def get_user_pages_or_reports(parent, cache=False):
|
||||
_cache = frappe.cache()
|
||||
|
||||
if cache:
|
||||
has_role = _cache.get_value('has_role:' + parent, user=frappe.session.user)
|
||||
if has_role:
|
||||
return has_role
|
||||
|
||||
def get_user_pages_or_reports(parent):
|
||||
roles = frappe.get_roles()
|
||||
has_role = {}
|
||||
column = get_column(parent)
|
||||
|
|
@ -184,6 +192,8 @@ def get_user_pages_or_reports(parent):
|
|||
for report in reports:
|
||||
has_role[report.name]["report_type"] = report.report_type
|
||||
|
||||
# Expire every six hours
|
||||
_cache.set_value('has_role:' + parent, has_role, frappe.session.user, 21600)
|
||||
return has_role
|
||||
|
||||
def get_column(doctype):
|
||||
|
|
|
|||
|
|
@ -16,11 +16,12 @@ global_cache_keys = ("app_hooks", "installed_apps",
|
|||
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
|
||||
'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version',
|
||||
'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts',
|
||||
'sitemap_routes')
|
||||
'sitemap_routes', 'db_tables')
|
||||
|
||||
user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
|
||||
"defaults", "user_permissions", "home_page", "linked_with",
|
||||
"desktop_icons", 'portal_menu_items')
|
||||
"desktop_icons", 'portal_menu_items', 'user_perm_can_read',
|
||||
"has_role:Page", "has_role:Report")
|
||||
|
||||
doctype_cache_keys = ("meta", "form_meta", "table_columns", "last_modified",
|
||||
"linked_doctypes", 'notifications', 'workflow' ,'energy_point_rule_map')
|
||||
|
|
|
|||
|
|
@ -43,14 +43,16 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin
|
|||
_new_site(db_name, site, mariadb_root_username=mariadb_root_username,
|
||||
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
|
||||
verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force,
|
||||
no_mariadb_socket=no_mariadb_socket, db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port)
|
||||
no_mariadb_socket=no_mariadb_socket, db_password=db_password, db_type=db_type, db_host=db_host,
|
||||
db_port=db_port, new_site=True)
|
||||
|
||||
if len(frappe.utils.get_sites()) == 1:
|
||||
use(site)
|
||||
|
||||
def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=None,
|
||||
admin_password=None, verbose=False, install_apps=None, source_sql=None, force=False,
|
||||
no_mariadb_socket=False, reinstall=False, db_password=None, db_type=None, db_host=None, db_port=None):
|
||||
no_mariadb_socket=False, reinstall=False, db_password=None, db_type=None, db_host=None,
|
||||
db_port=None, new_site=False):
|
||||
"""Install a new Frappe site"""
|
||||
|
||||
if not force and os.path.exists(site):
|
||||
|
|
@ -79,9 +81,12 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
|
|||
make_site_dirs()
|
||||
|
||||
installing = touch_file(get_site_path('locks', 'installing.lock'))
|
||||
atexit.register(_new_site_cleanup, site, mariadb_root_username, mariadb_root_password)
|
||||
|
||||
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, db_name=db_name,
|
||||
if new_site:
|
||||
# run cleanup only if new-site is called
|
||||
atexit.register(_new_site_cleanup, site, mariadb_root_username, mariadb_root_password)
|
||||
|
||||
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, db_name=db_name,
|
||||
admin_password=admin_password, verbose=verbose, source_sql=source_sql, force=force, reinstall=reinstall,
|
||||
db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port, no_mariadb_socket=no_mariadb_socket)
|
||||
apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
|
||||
|
|
@ -97,11 +102,14 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
|
|||
print("*** Scheduler is", scheduler_status, "***")
|
||||
|
||||
def _new_site_cleanup(site, mariadb_root_username, mariadb_root_password):
|
||||
installing = get_site_path('locks', 'installing.lock')
|
||||
try:
|
||||
installing = get_site_path('locks', 'installing.lock')
|
||||
except AttributeError:
|
||||
installing = os.path.join(site, 'locks', 'installing.lock')
|
||||
|
||||
if installing and os.path.exists(installing):
|
||||
if mariadb_root_password:
|
||||
_drop_site(site, mariadb_root_username, mariadb_root_password, force=True)
|
||||
_drop_site(site, mariadb_root_username, mariadb_root_password, force=True, no_backup=True)
|
||||
shutil.rmtree(site)
|
||||
|
||||
frappe.destroy()
|
||||
|
|
@ -256,6 +264,15 @@ def migrate(context, rebuild_website=False, skip_failing=False):
|
|||
print("Compiling Python Files...")
|
||||
compileall.compile_dir('../apps', quiet=1, rx=re.compile('.*node_modules.*'))
|
||||
|
||||
@click.command('migrate-to')
|
||||
@click.argument('frappe_provider')
|
||||
@pass_context
|
||||
def migrate_to(context, frappe_provider):
|
||||
"Migrates site to the specified provider"
|
||||
from frappe.integrations.frappe_providers import migrate_to
|
||||
for site in context.sites:
|
||||
migrate_to(site, frappe_provider)
|
||||
|
||||
@click.command('run-patch')
|
||||
@click.argument('module')
|
||||
@pass_context
|
||||
|
|
@ -317,23 +334,25 @@ def use(site, sites_path='.'):
|
|||
if os.path.exists(os.path.join(sites_path, site)):
|
||||
with open(os.path.join(sites_path, "currentsite.txt"), "w") as sitefile:
|
||||
sitefile.write(site)
|
||||
print("Current Site set to {}".format(site))
|
||||
else:
|
||||
print("{} does not exist".format(site))
|
||||
|
||||
@click.command('backup')
|
||||
@click.option('--with-files', default=False, is_flag=True, help="Take backup with files")
|
||||
@click.option('--verbose', default=False, is_flag=True)
|
||||
@pass_context
|
||||
def backup(context, with_files=False, backup_path_db=None, backup_path_files=None,
|
||||
backup_path_private_files=None, quiet=False):
|
||||
backup_path_private_files=None, quiet=False, verbose=False):
|
||||
"Backup"
|
||||
from frappe.utils.backups import scheduled_backup
|
||||
verbose = context.verbose
|
||||
verbose = verbose or context.verbose
|
||||
exit_code = 0
|
||||
for site in context.sites:
|
||||
try:
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True)
|
||||
odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True, verbose=verbose)
|
||||
except Exception as e:
|
||||
if verbose:
|
||||
print("Backup failed for {0}. Database or site_config.json may be corrupted".format(site))
|
||||
|
|
@ -342,10 +361,12 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non
|
|||
|
||||
if verbose:
|
||||
from frappe.utils import now
|
||||
print("database backup taken -", odb.backup_path_db, "- on", now())
|
||||
summary_title = "Backup Summary at {0}".format(now())
|
||||
print(summary_title + "\n" + "-" * len(summary_title))
|
||||
print("Database backup:", odb.backup_path_db)
|
||||
if with_files:
|
||||
print("files backup taken -", odb.backup_path_files, "- on", now())
|
||||
print("private files backup taken -", odb.backup_path_private_files, "- on", now())
|
||||
print("Public files: ", odb.backup_path_files)
|
||||
print("Private files: ", odb.backup_path_private_files)
|
||||
|
||||
frappe.destroy()
|
||||
sys.exit(exit_code)
|
||||
|
|
@ -559,6 +580,7 @@ commands = [
|
|||
install_app,
|
||||
list_apps,
|
||||
migrate,
|
||||
migrate_to,
|
||||
new_site,
|
||||
reinstall,
|
||||
reload_doc,
|
||||
|
|
|
|||
|
|
@ -443,7 +443,7 @@ def console(context):
|
|||
for app in all_apps:
|
||||
locals()[app] = __import__(app)
|
||||
print("Apps in this namespace:\n{}".format(", ".join(all_apps)))
|
||||
IPython.embed(display_banner="", header="")
|
||||
IPython.embed(display_banner="", header="", colors="neutral")
|
||||
|
||||
|
||||
@click.command('run-tests')
|
||||
|
|
@ -522,7 +522,7 @@ def run_ui_tests(context, app, headless=False):
|
|||
password_env = 'CYPRESS_adminPassword={}'.format(admin_password) if admin_password else ''
|
||||
|
||||
# run for headless mode
|
||||
run_or_open = 'run --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open'
|
||||
run_or_open = 'run --browser chrome --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open'
|
||||
command = '{site_env} {password_env} yarn run cypress {run_or_open}'
|
||||
formatted_command = command.format(site_env=site_env, password_env=password_env, run_or_open=run_or_open)
|
||||
frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from frappe import _
|
|||
|
||||
def get_data():
|
||||
return [
|
||||
{
|
||||
{
|
||||
"label": _("Form Customization"),
|
||||
"icon": "fa fa-glass",
|
||||
"items": [
|
||||
|
|
@ -57,9 +57,9 @@ def get_data():
|
|||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"label": _("Custom Tags"),
|
||||
"name": "Tag Category",
|
||||
"description": _("Add your own Tag Categories")
|
||||
"label": _("Package"),
|
||||
"name": "Package",
|
||||
"description": _("Import and Export Packages.")
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.utils import cstr, has_gravatar
|
||||
from frappe.utils import cstr, has_gravatar, cint
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links
|
||||
|
|
@ -123,7 +123,7 @@ class Contact(Document):
|
|||
def get_default_contact(doctype, name):
|
||||
'''Returns default contact for the given doctype, name'''
|
||||
out = frappe.db.sql('''select parent,
|
||||
(select is_primary_contact from tabContact c where c.name = dl.parent)
|
||||
IFNULL((select is_primary_contact from tabContact c where c.name = dl.parent), 0)
|
||||
as is_primary_contact
|
||||
from
|
||||
`tabDynamic Link` dl
|
||||
|
|
@ -133,7 +133,7 @@ def get_default_contact(doctype, name):
|
|||
dl.parenttype = "Contact"''', (doctype, name))
|
||||
|
||||
if out:
|
||||
return sorted(out, key = functools.cmp_to_key(lambda x,y: cmp(y[1], x[1])))[0][0]
|
||||
return sorted(out, key = functools.cmp_to_key(lambda x,y: cmp(cint(y[1]), cint(x[1]))))[0][0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"label": "Users",
|
||||
"modified": "2020-04-01 11:24:40.767676",
|
||||
"modified": "2020-04-26 22:36:14.311554",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Users",
|
||||
|
|
@ -46,12 +46,12 @@
|
|||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"label": "permission-manager",
|
||||
"label": "Permission Manager",
|
||||
"link_to": "permission-manager",
|
||||
"type": "Page"
|
||||
},
|
||||
{
|
||||
"label": "user-profile",
|
||||
"label": "User Profile",
|
||||
"link_to": "user-profile",
|
||||
"type": "Page"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class Comment(Document):
|
|||
def validate(self):
|
||||
if not self.comment_email:
|
||||
self.comment_email = frappe.session.user
|
||||
self.content = frappe.utils.sanitize_html(self.content)
|
||||
|
||||
def on_update(self):
|
||||
update_comment_in_doc(self)
|
||||
|
|
|
|||
|
|
@ -37,13 +37,11 @@ frappe.ui.form.on("Communication", {
|
|||
|
||||
if(frm.doc.status==="Open") {
|
||||
frm.add_custom_button(__("Close"), function() {
|
||||
frm.set_value("status", "Closed");
|
||||
frm.save();
|
||||
frm.trigger('mark_as_closed_open');
|
||||
});
|
||||
} else if (frm.doc.status !== "Linked") {
|
||||
frm.add_custom_button(__("Reopen"), function() {
|
||||
frm.set_value("status", "Open");
|
||||
frm.save();
|
||||
frm.trigger('mark_as_closed_open');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -61,30 +59,34 @@ frappe.ui.form.on("Communication", {
|
|||
|
||||
frm.add_custom_button(__("Reply All"), function() {
|
||||
frm.trigger('reply_all');
|
||||
}, "Actions");
|
||||
}, __("Actions"));
|
||||
|
||||
frm.add_custom_button(__("Forward"), function() {
|
||||
frm.trigger('forward_mail');
|
||||
}, "Actions");
|
||||
}, __("Actions"));
|
||||
|
||||
frm.add_custom_button(__("Mark as {0}", [frm.doc.seen? "Unread": "Read"]), function() {
|
||||
frm.add_custom_button(frm.doc.seen ? __("Mark as Unread") : __("Mark as Read"), function() {
|
||||
frm.trigger('mark_as_read_unread');
|
||||
}, "Actions");
|
||||
}, __("Actions"));
|
||||
|
||||
frm.add_custom_button(__("Add Contact"), function() {
|
||||
frm.trigger('add_to_contact');
|
||||
}, "Actions");
|
||||
frm.add_custom_button(__("Move"), function() {
|
||||
frm.trigger('show_move_dialog');
|
||||
}, __("Actions"));
|
||||
|
||||
if(frm.doc.email_status != "Spam")
|
||||
frm.add_custom_button(__("Mark as Spam"), function() {
|
||||
frm.trigger('mark_as_spam');
|
||||
}, "Actions");
|
||||
}, __("Actions"));
|
||||
|
||||
if(frm.doc.email_status != "Trash") {
|
||||
frm.add_custom_button(__("Move To Trash"), function() {
|
||||
frm.trigger('move_to_trash');
|
||||
}, "Actions");
|
||||
}, __("Actions"));
|
||||
}
|
||||
|
||||
frm.add_custom_button(__("Contact"), function() {
|
||||
frm.trigger('add_to_contact');
|
||||
}, __('Create'));
|
||||
}
|
||||
|
||||
if(frm.doc.communication_type=="Communication"
|
||||
|
|
@ -93,7 +95,7 @@ frappe.ui.form.on("Communication", {
|
|||
|
||||
frm.add_custom_button(__("Add Contact"), function() {
|
||||
frm.trigger('add_to_contact');
|
||||
}, "Actions");
|
||||
}, __("Actions"));
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -145,6 +147,43 @@ frappe.ui.form.on("Communication", {
|
|||
d.show();
|
||||
},
|
||||
|
||||
show_move_dialog: function(frm) {
|
||||
var d = new frappe.ui.Dialog ({
|
||||
title: __("Move"),
|
||||
fields: [{
|
||||
"fieldtype": "Link",
|
||||
"options": "Email Account",
|
||||
"label": __("Email Account"),
|
||||
"fieldname": "email_account",
|
||||
"reqd": 1,
|
||||
"get_query": function() {
|
||||
return {
|
||||
"filters": {
|
||||
"name": ["!=", frm.doc.email_account],
|
||||
"enable_incoming": ["=", 1]
|
||||
}
|
||||
};
|
||||
}
|
||||
}],
|
||||
primary_action_label: __("Move"),
|
||||
primary_action(values) {
|
||||
d.hide();
|
||||
frappe.call({
|
||||
method: "frappe.email.inbox.move_email",
|
||||
args: {
|
||||
communication: frm.doc.name,
|
||||
email_account: values.email_account
|
||||
},
|
||||
freeze: true,
|
||||
callback: function() {
|
||||
window.history.back();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
d.show();
|
||||
},
|
||||
|
||||
mark_as_read_unread: function(frm) {
|
||||
var action = frm.doc.seen? "Unread": "Read";
|
||||
var flag = "(\\SEEN)";
|
||||
|
|
@ -156,7 +195,26 @@ frappe.ui.form.on("Communication", {
|
|||
'action': action,
|
||||
'flag': flag
|
||||
},
|
||||
freeze: true
|
||||
freeze: true,
|
||||
callback: function() {
|
||||
frm.reload_doc();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
mark_as_closed_open: function(frm) {
|
||||
var status = frm.doc.status == "Open" ? "Closed" : "Open";
|
||||
|
||||
return frappe.call({
|
||||
method: "frappe.email.inbox.mark_as_closed_open",
|
||||
args: {
|
||||
communication: frm.doc.name,
|
||||
status: status
|
||||
},
|
||||
freeze: true,
|
||||
callback: function() {
|
||||
frm.reload_doc();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -197,6 +197,7 @@
|
|||
"label": "More Information"
|
||||
},
|
||||
{
|
||||
"bold": 0,
|
||||
"default": "Now",
|
||||
"fieldname": "communication_date",
|
||||
"fieldtype": "Datetime",
|
||||
|
|
@ -424,6 +425,15 @@
|
|||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export":1,
|
||||
"print":1,
|
||||
"read": 1,
|
||||
"role": "Inbox User"
|
||||
},
|
||||
{
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
|
|
|
|||
|
|
@ -2,20 +2,21 @@
|
|||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals, absolute_import
|
||||
from collections import Counter
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import validate_email_address, get_fullname, strip_html, cstr
|
||||
from frappe.core.doctype.communication.email import (validate_email,
|
||||
notify, _notify, update_parent_mins_to_first_response)
|
||||
from frappe.utils import validate_email_address, strip_html, cstr, time_diff_in_seconds
|
||||
from frappe.core.doctype.communication.email import validate_email, notify, _notify
|
||||
from frappe.core.utils import get_parent_doc
|
||||
from frappe.utils.bot import BotReply
|
||||
from frappe.utils import parse_addr
|
||||
from frappe.core.doctype.comment.comment import update_comment_in_doc
|
||||
from email.utils import parseaddr
|
||||
from six.moves.urllib.parse import unquote
|
||||
from collections import Counter
|
||||
from frappe.utils.user import is_system_user
|
||||
from frappe.contacts.doctype.contact.contact import get_contact_name
|
||||
from frappe.automation.doctype.assignment_rule.assignment_rule import apply as apply_assignment_rule
|
||||
|
||||
exclude_from_linked_with = True
|
||||
|
||||
|
|
@ -119,7 +120,7 @@ class Communication(Document):
|
|||
update_comment_in_doc(self)
|
||||
|
||||
if self.comment_type != 'Updated':
|
||||
update_parent_mins_to_first_response(self)
|
||||
update_parent_document_on_communication(self)
|
||||
self.bot_reply()
|
||||
|
||||
def on_trash(self):
|
||||
|
|
@ -258,7 +259,12 @@ class Communication(Document):
|
|||
|
||||
# Timeline Links
|
||||
def set_timeline_links(self):
|
||||
contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc])
|
||||
contacts = []
|
||||
if (self.email_account and frappe.db.get_value("Email Account", self.email_account, "create_contact")) or \
|
||||
frappe.flags.in_test:
|
||||
|
||||
contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc])
|
||||
|
||||
for contact_name in contacts:
|
||||
self.add_link('Contact', contact_name)
|
||||
|
||||
|
|
@ -423,3 +429,39 @@ def get_email_without_link(email):
|
|||
email_host = email.split("@")[1]
|
||||
|
||||
return "{0}@{1}".format(email_id, email_host)
|
||||
|
||||
def update_parent_document_on_communication(doc):
|
||||
"""Update mins_to_first_communication of parent document based on who is replying."""
|
||||
|
||||
parent = get_parent_doc(doc)
|
||||
if not parent:
|
||||
return
|
||||
|
||||
# update parent mins_to_first_communication only if we create the Email communication
|
||||
# ignore in case of only Comment is added
|
||||
if doc.communication_type == "Comment":
|
||||
return
|
||||
|
||||
status_field = parent.meta.get_field("status")
|
||||
if status_field:
|
||||
options = (status_field.options or '').splitlines()
|
||||
|
||||
# if status has a "Replied" option, then update the status for received communication
|
||||
if ('Replied' in options) and doc.sent_or_received=="Received":
|
||||
parent.db_set("status", "Open")
|
||||
apply_assignment_rule(parent)
|
||||
else:
|
||||
# update the modified date for document
|
||||
parent.update_modified()
|
||||
|
||||
update_mins_to_first_communication(parent, doc)
|
||||
parent.run_method('notify_communication', doc)
|
||||
parent.notify_update()
|
||||
|
||||
def update_mins_to_first_communication(parent, communication):
|
||||
if parent.meta.has_field('mins_to_first_response') and not parent.get('mins_to_first_response'):
|
||||
if is_system_user(communication.sender):
|
||||
first_responded_on = communication.creation
|
||||
if parent.meta.has_field('first_responded_on') and communication.sent_or_received == "Sent":
|
||||
parent.db_set('first_responded_on', first_responded_on)
|
||||
parent.db_set('mins_to_first_response', round(time_diff_in_seconds(first_responded_on, parent.creation) / 60), 2)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ frappe.listview_settings['Communication'] = {
|
|||
"sent_or_received","recipients", "subject",
|
||||
"communication_medium", "communication_type",
|
||||
"sender", "seen", "reference_doctype", "reference_name",
|
||||
"has_attachment"
|
||||
"has_attachment", "communication_date"
|
||||
],
|
||||
|
||||
filters: [["status", "=", "Open"]],
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import json
|
|||
from email.utils import formataddr
|
||||
from frappe.core.utils import get_parent_doc
|
||||
from frappe.utils import (get_url, get_formatted_email, cint,
|
||||
validate_email_address, split_emails, time_diff_in_seconds, parse_addr, get_datetime)
|
||||
validate_email_address, split_emails, parse_addr, get_datetime)
|
||||
from frappe.email.email_body import get_message_id
|
||||
import frappe.email.smtp
|
||||
import time
|
||||
|
|
@ -172,33 +172,6 @@ def _notify(doc, print_html=None, print_format=None, attachments=None,
|
|||
print_letterhead=frappe.flags.print_letterhead
|
||||
)
|
||||
|
||||
def update_parent_mins_to_first_response(doc):
|
||||
"""Update mins_to_first_communication of parent document based on who is replying."""
|
||||
|
||||
parent = get_parent_doc(doc)
|
||||
if not parent:
|
||||
return
|
||||
|
||||
# update parent mins_to_first_communication only if we create the Email communication
|
||||
# ignore in case of only Comment is added
|
||||
if doc.communication_type == "Comment":
|
||||
return
|
||||
|
||||
status_field = parent.meta.get_field("status")
|
||||
if status_field:
|
||||
options = (status_field.options or '').splitlines()
|
||||
|
||||
# if status has a "Replied" option, then update the status for received communication
|
||||
if ('Replied' in options) and doc.sent_or_received=="Received":
|
||||
parent.db_set("status", "Open")
|
||||
else:
|
||||
# update the modified date for document
|
||||
parent.update_modified()
|
||||
|
||||
update_mins_to_first_communication(parent, doc)
|
||||
parent.run_method('notify_communication', doc)
|
||||
parent.notify_update()
|
||||
|
||||
def get_recipients_cc_and_bcc(doc, recipients, cc, bcc, fetched_from_email_account=False):
|
||||
doc.all_email_addresses = []
|
||||
doc.sent_email_addresses = []
|
||||
|
|
@ -499,15 +472,6 @@ def sendmail(communication_name, print_html=None, print_format=None, attachments
|
|||
traceback = frappe.log_error("frappe.core.doctype.communication.email.sendmail")
|
||||
raise
|
||||
|
||||
def update_mins_to_first_communication(parent, communication):
|
||||
if parent.meta.has_field('mins_to_first_response') and not parent.get('mins_to_first_response'):
|
||||
if frappe.db.get_all('User', filters={'email': communication.sender,
|
||||
'user_type': 'System User', 'enabled': 1}, limit=1):
|
||||
first_responded_on = communication.creation
|
||||
if parent.meta.has_field('first_responded_on') and communication.sent_or_received == "Sent":
|
||||
parent.db_set('first_responded_on', first_responded_on)
|
||||
parent.db_set('mins_to_first_response', round(time_diff_in_seconds(first_responded_on, parent.creation) / 60), 2)
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def mark_email_as_seen(name=None):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -202,6 +202,8 @@ class TestCommunication(unittest.TestCase):
|
|||
self.assertIn(("Note", note.name), doc_links)
|
||||
|
||||
def create_email_account():
|
||||
frappe.delete_doc_if_exists("Email Account", "_Test Comm Account 1")
|
||||
|
||||
frappe.flags.mute_emails = False
|
||||
frappe.flags.sent_mail = None
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,11 @@ class Exporter:
|
|||
return parent_fields
|
||||
|
||||
def get_exportable_children_fields(self):
|
||||
children = [df.options for df in self.meta.fields if df.fieldtype in table_fields]
|
||||
child_table_fields = [df for df in self.meta.fields if df.fieldtype in table_fields]
|
||||
if self.export_fields == "Mandatory":
|
||||
child_table_fields = [df for df in child_table_fields if df.reqd]
|
||||
|
||||
children = [df.options for df in child_table_fields]
|
||||
children_fields = []
|
||||
for child in children:
|
||||
children_fields += self.get_exportable_fields(child)
|
||||
|
|
|
|||
|
|
@ -74,7 +74,6 @@ class Importer:
|
|||
self.read_content(content, extension)
|
||||
|
||||
self.validate_template_content()
|
||||
self.remove_empty_rows_and_columns()
|
||||
|
||||
def read_file(self, file_path):
|
||||
extn = file_path.split(".")[1]
|
||||
|
|
@ -99,6 +98,8 @@ class Importer:
|
|||
elif extension == "xls":
|
||||
data = read_xls_file_from_attached_file(content)
|
||||
|
||||
data = self.remove_empty_rows_and_columns(data)
|
||||
|
||||
if len(data) <= 1:
|
||||
frappe.throw(
|
||||
_("Import template should contain a Header and atleast one row."), title=error_title
|
||||
|
|
@ -114,42 +115,41 @@ class Importer:
|
|||
_("Number of columns does not match with data"), title=_("Invalid Template")
|
||||
)
|
||||
|
||||
def remove_empty_rows_and_columns(self):
|
||||
def remove_empty_rows_and_columns(self, raw_data):
|
||||
self.row_index_map = []
|
||||
removed_rows = []
|
||||
removed_columns = []
|
||||
|
||||
# remove empty rows
|
||||
data = []
|
||||
for i, row in enumerate(self.data):
|
||||
data_without_empty_rows = []
|
||||
for i, row in enumerate(raw_data):
|
||||
if all(v in INVALID_VALUES for v in row):
|
||||
# empty row
|
||||
removed_rows.append(i)
|
||||
else:
|
||||
data.append(row)
|
||||
data_without_empty_rows.append(row)
|
||||
self.row_index_map.append(i)
|
||||
|
||||
# remove empty columns
|
||||
# a column with a header and no data is a valid column
|
||||
# a column with no header and no data will be removed
|
||||
header_row = []
|
||||
for i, column in enumerate(self.header_row):
|
||||
column_values = [row[i] for row in data]
|
||||
values = [column] + column_values
|
||||
if all(v in INVALID_VALUES for v in values):
|
||||
first_row = data_without_empty_rows[0]
|
||||
for i, column in enumerate(first_row):
|
||||
column_values = [row[i] for row in data_without_empty_rows]
|
||||
if all(v in INVALID_VALUES for v in column_values):
|
||||
# empty column
|
||||
removed_columns.append(i)
|
||||
else:
|
||||
header_row.append(column)
|
||||
|
||||
data_without_empty_columns = []
|
||||
# remove empty columns from data
|
||||
for i, row in enumerate(data):
|
||||
new_row = [v for j, v in enumerate(row) if j not in removed_columns]
|
||||
data_without_empty_columns.append(new_row)
|
||||
if removed_columns:
|
||||
data_without_empty_rows_and_columns = []
|
||||
# remove empty columns from data
|
||||
for i, row in enumerate(data_without_empty_rows):
|
||||
new_row = [v for j, v in enumerate(row) if j not in removed_columns]
|
||||
data_without_empty_rows_and_columns.append(new_row)
|
||||
else:
|
||||
data_without_empty_rows_and_columns = data_without_empty_rows
|
||||
|
||||
self.data = data_without_empty_columns
|
||||
self.header_row = header_row
|
||||
return data_without_empty_rows_and_columns
|
||||
|
||||
def get_data_for_import_preview(self):
|
||||
out = frappe._dict()
|
||||
|
|
@ -325,7 +325,7 @@ class Importer:
|
|||
|
||||
def detect_date_formats(self, columns):
|
||||
for col in columns:
|
||||
if col.df and col.df.fieldtype in ['Date', 'Time', 'Datetime']:
|
||||
if col.df and col.df.fieldtype in ["Date", "Time", "Datetime"]:
|
||||
col.date_format = self.guess_date_format_for_column(col, columns)
|
||||
return columns
|
||||
|
||||
|
|
@ -351,9 +351,18 @@ class Importer:
|
|||
value = cstr(value)
|
||||
|
||||
# convert boolean values to 0 or 1
|
||||
if df.fieldtype == "Check" and value.lower().strip() in ["t", "f", "true", "false"]:
|
||||
if df.fieldtype == "Check" and value.lower().strip() in [
|
||||
"t",
|
||||
"f",
|
||||
"true",
|
||||
"false",
|
||||
"yes",
|
||||
"no",
|
||||
"y",
|
||||
"n",
|
||||
]:
|
||||
value = value.lower().strip()
|
||||
value = 1 if value in ["t", "true"] else 0
|
||||
value = 1 if value in ["t", "true", "y", "yes"] else 0
|
||||
|
||||
if df.fieldtype in ["Int", "Check"]:
|
||||
value = cint(value)
|
||||
|
|
@ -398,8 +407,9 @@ class Importer:
|
|||
date_values = [
|
||||
row[column_index] for row in self.data[:PARSE_ROW_COUNT] if row[column_index]
|
||||
]
|
||||
date_formats = [guess_date_format(d) if isinstance(d, str) else None
|
||||
for d in date_values]
|
||||
date_formats = [
|
||||
guess_date_format(d) if isinstance(d, str) else None for d in date_values
|
||||
]
|
||||
if not date_formats:
|
||||
return
|
||||
max_occurred_date_format = max(set(date_formats), key=date_formats.count)
|
||||
|
|
@ -610,7 +620,7 @@ class Importer:
|
|||
"message": msg,
|
||||
}
|
||||
)
|
||||
return False
|
||||
return
|
||||
|
||||
elif df.fieldtype == "Link":
|
||||
d = self.get_missing_link_field_values(df.options)
|
||||
|
|
@ -643,8 +653,10 @@ class Importer:
|
|||
if value in INVALID_VALUES:
|
||||
value = None
|
||||
|
||||
value = validate_value(value, df)
|
||||
if value:
|
||||
if value is not None:
|
||||
value = validate_value(value, df)
|
||||
|
||||
if value is not None:
|
||||
doc[df.fieldname] = self.parse_value(value, df)
|
||||
|
||||
is_table = frappe.get_meta(doctype).istable
|
||||
|
|
@ -825,9 +837,9 @@ class Importer:
|
|||
id_value = doc[id_fieldname]
|
||||
existing_doc = frappe.get_doc(self.doctype, id_value)
|
||||
existing_doc.flags.updater_reference = {
|
||||
'doctype': self.data_import.doctype,
|
||||
'docname': self.data_import.name,
|
||||
'label': _('via Data Import')
|
||||
"doctype": self.data_import.doctype,
|
||||
"docname": self.data_import.name,
|
||||
"label": _("via Data Import"),
|
||||
}
|
||||
existing_doc.update(doc)
|
||||
existing_doc.save()
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class TestExporter(unittest.TestCase):
|
|||
e = Exporter('Web Page', export_fields='All')
|
||||
csv_array = e.get_csv_array()
|
||||
header = csv_array[0]
|
||||
self.assertEqual(len(header), 28)
|
||||
self.assertEqual(len(header), 37)
|
||||
|
||||
|
||||
def test_exports_selected_fields(self):
|
||||
|
|
|
|||
|
|
@ -177,8 +177,8 @@ frappe.ui.form.on('Data Import Beta', {
|
|||
start_import(frm) {
|
||||
frm
|
||||
.call({
|
||||
doc: frm.doc,
|
||||
method: 'start_import',
|
||||
method: 'form_start_import',
|
||||
args: { data_import: frm.doc.name },
|
||||
btn: frm.page.btn_primary
|
||||
})
|
||||
.then(r => {
|
||||
|
|
@ -252,8 +252,8 @@ frappe.ui.form.on('Data Import Beta', {
|
|||
|
||||
frm
|
||||
.call({
|
||||
doc: frm.doc,
|
||||
method: 'get_preview_from_template',
|
||||
args: { data_import: frm.doc.name },
|
||||
error_handlers: {
|
||||
TimestampMismatchError() {
|
||||
// ignore this error
|
||||
|
|
@ -337,7 +337,12 @@ frappe.ui.form.on('Data Import Beta', {
|
|||
let message = warnings_by_row[row_number]
|
||||
.map(w => {
|
||||
if (w.field) {
|
||||
return `<li>${w.field.label}: ${w.message}</li>`;
|
||||
let label =
|
||||
w.field.label +
|
||||
(w.field.parent !== frm.doc.reference_doctype
|
||||
? ` (${w.field.parent})`
|
||||
: '');
|
||||
return `<li>${label}: ${w.message}</li>`;
|
||||
}
|
||||
return `<li>${w.message}</li>`;
|
||||
})
|
||||
|
|
|
|||
|
|
@ -61,6 +61,16 @@ class DataImportBeta(Document):
|
|||
return Importer(self.reference_doctype, data_import=self)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_preview_from_template(data_import):
|
||||
return frappe.get_doc("Data Import Beta", data_import).get_preview_from_template()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def form_start_import(data_import):
|
||||
return frappe.get_doc("Data Import Beta", data_import).start_import()
|
||||
|
||||
|
||||
def start_import(data_import):
|
||||
"""This method runs in background job"""
|
||||
data_import = frappe.get_doc("Data Import Beta", data_import)
|
||||
|
|
@ -69,12 +79,11 @@ def start_import(data_import):
|
|||
i.import_data()
|
||||
except:
|
||||
frappe.db.rollback()
|
||||
data_import.db_set('status', 'Error')
|
||||
data_import.db_set("status", "Error")
|
||||
frappe.log_error(title=data_import.name)
|
||||
frappe.db.commit()
|
||||
frappe.publish_realtime(
|
||||
"data_import_refresh", {"data_import": data_import.name}
|
||||
)
|
||||
frappe.publish_realtime("data_import_refresh", {"data_import": data_import.name})
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_template(
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
"fieldname",
|
||||
"precision",
|
||||
"length",
|
||||
"show_days",
|
||||
"show_seconds",
|
||||
"reqd",
|
||||
"search_index",
|
||||
"in_list_view",
|
||||
|
|
@ -43,6 +45,7 @@
|
|||
"report_hide",
|
||||
"remember_last_selected_value",
|
||||
"ignore_xss_filter",
|
||||
"hide_border",
|
||||
"property_depends_on_section",
|
||||
"mandatory_depends_on",
|
||||
"column_break_38",
|
||||
|
|
@ -86,7 +89,7 @@
|
|||
"label": "Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
|
|
@ -448,12 +451,33 @@
|
|||
{
|
||||
"fieldname": "column_break_38",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:doc.fieldtype === \"Duration\";",
|
||||
"fieldname": "show_days",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Days"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:doc.fieldtype === \"Duration\";",
|
||||
"fieldname": "show_seconds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Seconds"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Section Break'",
|
||||
"fieldname": "hide_border",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Border"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-19 21:54:13.783908",
|
||||
"modified": "2020-05-15 09:06:25.224411",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocField",
|
||||
|
|
|
|||
|
|
@ -712,9 +712,10 @@ def validate_fields(meta):
|
|||
if d.fieldtype == "Currency" and cint(d.width) < 100:
|
||||
frappe.throw(_("Max width for type Currency is 100px in row {0}").format(d.idx))
|
||||
|
||||
def check_in_list_view(d):
|
||||
def check_in_list_view(is_table, d):
|
||||
if d.in_list_view and (d.fieldtype in not_allowed_in_list_view):
|
||||
frappe.throw(_("'In List View' not allowed for type {0} in row {1}").format(d.fieldtype, d.idx))
|
||||
property_label = 'In Grid View' if is_table else 'In List View'
|
||||
frappe.throw(_("'{0}' not allowed for type {1} in row {2}").format(property_label, d.fieldtype, d.idx))
|
||||
|
||||
def check_in_global_search(d):
|
||||
if d.in_global_search and d.fieldtype in no_value_fields:
|
||||
|
|
@ -733,8 +734,11 @@ def validate_fields(meta):
|
|||
d.default = '0'
|
||||
if d.fieldtype == "Check" and d.default not in ('0', '1'):
|
||||
frappe.throw(_("Default for 'Check' type of field must be either '0' or '1'"))
|
||||
if d.fieldtype == "Select" and d.default and (d.default not in d.options.split("\n")):
|
||||
frappe.throw(_("Default for {0} must be an option").format(d.fieldname))
|
||||
if d.fieldtype == "Select" and d.default:
|
||||
if not d.options:
|
||||
frappe.throw(_("Options for {0} must be set before setting the default value.").format(frappe.bold(d.fieldname)))
|
||||
elif d.default not in d.options.split("\n"):
|
||||
frappe.throw(_("Default value for {0} must be in the list of options.").format(frappe.bold(d.fieldname)))
|
||||
|
||||
def check_precision(d):
|
||||
if d.fieldtype in ("Currency", "Float", "Percent") and d.precision is not None and not (1 <= cint(d.precision) <= 6):
|
||||
|
|
@ -903,6 +907,16 @@ def validate_fields(meta):
|
|||
|
||||
frappe.msgprint(text_str + df_options_str, title="Invalid Data Field", raise_exception=True)
|
||||
|
||||
def check_child_table_option(docfield):
|
||||
if docfield.fieldtype not in ['Table MultiSelect', 'Table']: return
|
||||
|
||||
doctype = docfield.options
|
||||
meta = frappe.get_meta(doctype)
|
||||
|
||||
if not meta.istable:
|
||||
frappe.throw(_('Option {0} for field {1} is not a child table')
|
||||
.format(frappe.bold(doctype), frappe.bold(docfield.fieldname)), title=_("Invalid Option"))
|
||||
|
||||
|
||||
fields = meta.get("fields")
|
||||
fieldname_list = [d.fieldname for d in fields]
|
||||
|
|
@ -926,11 +940,12 @@ def validate_fields(meta):
|
|||
check_link_table_options(meta.get("name"), d)
|
||||
check_dynamic_link_options(d)
|
||||
check_hidden_and_mandatory(meta.get("name"), d)
|
||||
check_in_list_view(d)
|
||||
check_in_list_view(meta.get('istable'), d)
|
||||
check_in_global_search(d)
|
||||
check_illegal_default(d)
|
||||
check_unique_and_text(meta.get("name"), d)
|
||||
check_illegal_depends_on_conditions(d)
|
||||
check_child_table_option(d)
|
||||
check_table_multiselect_option(d)
|
||||
scrub_options_in_select(d)
|
||||
scrub_fetch_from(d)
|
||||
|
|
|
|||
|
|
@ -1,46 +1,45 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
"""
|
||||
record of files
|
||||
|
||||
naming for same name files: file.gif, file-1.gif, file-2.gif etc
|
||||
"""
|
||||
|
||||
import frappe
|
||||
import json
|
||||
import os
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import base64
|
||||
import re
|
||||
import hashlib
|
||||
import mimetypes
|
||||
import imghdr
|
||||
import io
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import zipfile
|
||||
|
||||
import requests
|
||||
import requests.exceptions
|
||||
import imghdr
|
||||
from PIL import Image, ImageFile, ImageOps
|
||||
from six import PY2, StringIO, string_types, text_type
|
||||
from six.moves.urllib.parse import quote, unquote
|
||||
|
||||
from frappe.utils import get_hook_method, get_files_path, random_string, encode, cstr, call_hook_method, cint
|
||||
from frappe import _
|
||||
from frappe import conf
|
||||
from frappe.utils.nestedset import NestedSet
|
||||
import frappe
|
||||
from frappe import _, conf
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import strip
|
||||
from PIL import Image, ImageOps
|
||||
from six import StringIO, string_types
|
||||
from six.moves.urllib.parse import unquote, quote
|
||||
from six import text_type, PY2
|
||||
import zipfile
|
||||
from frappe.utils import call_hook_method, cint, cstr, encode, get_files_path, get_hook_method, random_string, strip
|
||||
|
||||
|
||||
class MaxFileSizeReachedError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class FolderNotEmpty(frappe.ValidationError): pass
|
||||
class FolderNotEmpty(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
exclude_from_linked_with = True
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
|
||||
|
||||
class File(Document):
|
||||
|
|
@ -697,7 +696,7 @@ def remove_file(fid=None, attached_to_doctype=None, attached_to_name=None, from_
|
|||
|
||||
|
||||
def get_max_file_size():
|
||||
return conf.get('max_file_size') or 10485760
|
||||
return cint(conf.get('max_file_size')) or 10485760
|
||||
|
||||
|
||||
def remove_all(dt, dn, from_delete=False):
|
||||
|
|
@ -714,7 +713,10 @@ def has_permission(doc, ptype=None, user=None):
|
|||
has_access = False
|
||||
user = user or frappe.session.user
|
||||
|
||||
if not doc.is_private or doc.owner == user or user == 'Administrator':
|
||||
if ptype == 'create':
|
||||
has_access = frappe.has_permission('File', 'create', user=user)
|
||||
|
||||
if not doc.is_private or doc.owner in [user, 'Guest'] or user == 'Administrator':
|
||||
has_access = True
|
||||
|
||||
if doc.attached_to_doctype and doc.attached_to_name:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2020-05-11 17:44:54.674657",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"app_name",
|
||||
"app_version",
|
||||
"git_branch"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "git_branch",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Git Branch",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "app_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Application Name",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "app_version",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Application Version",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-05-12 10:09:49.148087",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Installed Application",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class OnboardingSlideField(Document):
|
||||
class InstalledApplication(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2020, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Installed Applications', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2020-05-11 17:45:41.587750",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"installed_applications"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "installed_applications",
|
||||
"fieldtype": "Table",
|
||||
"label": "Installed Applications",
|
||||
"options": "Installed Application",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-05-12 10:09:14.310622",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Installed Applications",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class InstalledApplications(Document):
|
||||
def update_versions(self):
|
||||
self.delete_key("installed_applications")
|
||||
for app in frappe.utils.get_installed_apps_info():
|
||||
self.append("installed_applications", {
|
||||
"app_name": app.get("app_name"),
|
||||
"app_version": app.get("version"),
|
||||
"git_branch": app.get("branch")
|
||||
})
|
||||
self.save()
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestInstalledApplications(unittest.TestCase):
|
||||
pass
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
"column_break_3",
|
||||
"time_zone",
|
||||
"is_first_startup",
|
||||
"enable_onboarding",
|
||||
"setup_complete",
|
||||
"date_and_number_format",
|
||||
"date_format",
|
||||
|
|
@ -375,6 +376,7 @@
|
|||
"label": "Hide footer in auto email reports"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "chat",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Chat"
|
||||
|
|
@ -414,12 +416,18 @@
|
|||
"fieldname": "logout_on_password_reset",
|
||||
"fieldtype": "Check",
|
||||
"label": "Logout All Sessions on Password Reset"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enable_onboarding",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Onboarding"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-03-16 14:50:40.914532",
|
||||
"modified": "2020-05-01 19:21:15.496065",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "System Settings",
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ def get_translation_data():
|
|||
def create_translation(key, val):
|
||||
translation = frappe.new_doc('Translation')
|
||||
translation.language = key
|
||||
translation.source_name = val[0]
|
||||
translation.target_name = val[1]
|
||||
translation.source_text = val[0]
|
||||
translation.translated_text = val[1]
|
||||
translation.save()
|
||||
return translation
|
||||
|
|
|
|||
|
|
@ -3,19 +3,7 @@
|
|||
|
||||
|
||||
frappe.ui.form.on('Translation', {
|
||||
refresh: function(frm) {
|
||||
if(frm.is_new() || !(["Saved", "Deleted"].includes(frm.doc.status))) return;
|
||||
frm.add_custom_button('Contribute', function() {
|
||||
frappe.call({
|
||||
method: 'frappe.core.doctype.translation.translation.contribute_translation',
|
||||
args: {
|
||||
"language": frm.doc.language,
|
||||
"contributor": frm.doc.owner,
|
||||
"source_name": frm.doc.source_name,
|
||||
"target_name": frm.doc.target_name,
|
||||
"doc_name": frm.doc.name
|
||||
}
|
||||
});
|
||||
}).addClass('btn-primary');
|
||||
refresh: function() {
|
||||
//
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
{
|
||||
"_comments": "[]",
|
||||
"_liked_by": "[]",
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"autoname": "hash",
|
||||
"creation": "2016-02-17 12:21:16.175465",
|
||||
|
|
@ -6,20 +9,21 @@
|
|||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"contributed",
|
||||
"language",
|
||||
"section_break_4",
|
||||
"source_name",
|
||||
"source_text",
|
||||
"context",
|
||||
"column_break_6",
|
||||
"target_name",
|
||||
"translated_text",
|
||||
"section_break_6",
|
||||
"status",
|
||||
"contributed_translation_doctype_name"
|
||||
"contribution_status",
|
||||
"contribution_docname"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "language",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Language",
|
||||
"options": "Language",
|
||||
"search_index": 1
|
||||
|
|
@ -28,44 +32,58 @@
|
|||
"fieldname": "section_break_4",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"description": "If your data is in HTML, please copy paste the exact HTML code with the tags.",
|
||||
"fieldname": "source_name",
|
||||
"fieldtype": "Code",
|
||||
"label": "Source Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "target_name",
|
||||
"fieldtype": "Code",
|
||||
"in_list_view": 1,
|
||||
"label": "Translated Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "Saved",
|
||||
"depends_on": "eval: !doc.__islocal",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"options": "Saved\nContributed\nVerified\nPR sent\nDeleted",
|
||||
"fieldname": "context",
|
||||
"fieldtype": "Data",
|
||||
"label": "Context",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "contributed_translation_doctype_name",
|
||||
"default": "0",
|
||||
"fieldname": "contributed",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Contributed",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "doc.contributed",
|
||||
"fieldname": "contribution_status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Contribution Status",
|
||||
"options": "\nPending\nVerified\nRejected",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "contribution_docname",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Contributed Translation Doctype Name",
|
||||
"label": "Contribution Document Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"description": "If your data is in HTML, please copy paste the exact HTML code with the tags.",
|
||||
"fieldname": "source_text",
|
||||
"fieldtype": "Code",
|
||||
"label": "Source Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "translated_text",
|
||||
"fieldtype": "Code",
|
||||
"in_list_view": 1,
|
||||
"label": "Translated Text"
|
||||
}
|
||||
],
|
||||
"modified": "2019-06-18 19:03:38.640990",
|
||||
"links": [],
|
||||
"modified": "2020-03-12 13:28:48.223409",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Translation",
|
||||
|
|
@ -86,6 +104,6 @@
|
|||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "source_name",
|
||||
"title_field": "source_text",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -5,57 +5,77 @@
|
|||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.translate import clear_cache
|
||||
from frappe.utils import strip_html_tags, is_html
|
||||
from frappe.integrations.utils import make_post_request
|
||||
from frappe.translate import get_translator_url
|
||||
import json
|
||||
|
||||
class Translation(Document):
|
||||
def validate(self):
|
||||
if is_html(self.source_name):
|
||||
if is_html(self.source_text):
|
||||
self.remove_html_from_source()
|
||||
|
||||
def remove_html_from_source(self):
|
||||
self.source_name = strip_html_tags(self.source_name).strip()
|
||||
self.source_text = strip_html_tags(self.source_text).strip()
|
||||
|
||||
def on_update(self):
|
||||
clear_cache()
|
||||
clear_user_translation_cache(self.language)
|
||||
|
||||
def on_trash(self):
|
||||
clear_cache()
|
||||
clear_user_translation_cache(self.language)
|
||||
|
||||
def onload(self):
|
||||
if self.contributed_translation_doctype_name:
|
||||
data = {"data": json.dumps({
|
||||
"doc_name": self.contributed_translation_doctype_name
|
||||
})}
|
||||
try:
|
||||
response = make_post_request(url=frappe.get_hooks("translation_contribution_status")[0], data=data)
|
||||
except Exception:
|
||||
frappe.msgprint("Something went wrong. Please check error log for more details")
|
||||
if response.get("message").get("message") == "Contributed Translation has been deleted":
|
||||
self.status = "Deleted"
|
||||
self.contributed_translation_doctype_name = ""
|
||||
self.save()
|
||||
else:
|
||||
self.status = response.get("message").get("status")
|
||||
self.save()
|
||||
def contribute(self):
|
||||
pass
|
||||
|
||||
def get_contribution_status(self):
|
||||
pass
|
||||
|
||||
@frappe.whitelist()
|
||||
def contribute_translation(language, contributor, source_name, target_name, doc_name):
|
||||
data = {"data": json.dumps({
|
||||
"language": language,
|
||||
"contributor": contributor,
|
||||
"source_name": source_name,
|
||||
"target_name": target_name,
|
||||
"posting_date": frappe.utils.nowdate()
|
||||
})}
|
||||
try:
|
||||
response = make_post_request(url=frappe.get_hooks("translation_contribution_url")[0], data=data)
|
||||
except Exception:
|
||||
frappe.msgprint("Something went wrong while contributing translation. Please check error log for more details")
|
||||
if response.get("message").get("message") == "Already exists":
|
||||
frappe.msgprint("Translation already exists")
|
||||
elif response.get("message").get("message") == "Added to contribution list":
|
||||
frappe.set_value("Translation", doc_name, "contributed_translation_doctype_name", response.get("message").get("doc_name"))
|
||||
frappe.msgprint("Translation successfully contributed")
|
||||
def create_translations(translation_map, language):
|
||||
from frappe.frappeclient import FrappeClient
|
||||
|
||||
translation_map = json.loads(translation_map)
|
||||
translation_map_to_send = frappe._dict({})
|
||||
# first create / update local user translations
|
||||
for source_id, translation_dict in translation_map.items():
|
||||
translation_dict = frappe._dict(translation_dict)
|
||||
existing_doc_name = frappe.db.get_all('Translation', {
|
||||
'source_text': translation_dict.source_text,
|
||||
'context': translation_dict.context or '',
|
||||
'language': language,
|
||||
})
|
||||
translation_map_to_send[source_id] = translation_dict
|
||||
if existing_doc_name:
|
||||
frappe.db.set_value('Translation', existing_doc_name[0].name, {
|
||||
'translated_text': translation_dict.translated_text,
|
||||
'contributed': 1,
|
||||
'contribution_status': 'Pending'
|
||||
})
|
||||
translation_map_to_send[source_id].name = existing_doc_name[0].name
|
||||
else:
|
||||
doc = frappe.get_doc({
|
||||
'doctype': 'Translation',
|
||||
'source_text': translation_dict.source_text,
|
||||
'contributed': 1,
|
||||
'contribution_status': 'Pending',
|
||||
'translated_text': translation_dict.translated_text,
|
||||
'context': translation_dict.context,
|
||||
'language': language
|
||||
})
|
||||
doc.insert()
|
||||
translation_map_to_send[source_id].name = doc.name
|
||||
|
||||
params = {
|
||||
'language': language,
|
||||
'contributor_email': frappe.session.user,
|
||||
'contributor_name': frappe.utils.get_fullname(frappe.session.user),
|
||||
'translation_map': json.dumps(translation_map_to_send)
|
||||
}
|
||||
|
||||
translator = FrappeClient(get_translator_url())
|
||||
added_translations = translator.post_api('translator.api.add_translations', params=params)
|
||||
|
||||
for local_docname, remote_docname in added_translations.items():
|
||||
frappe.db.set_value('Translation', local_docname, 'contribution_docname', remote_docname)
|
||||
|
||||
def clear_user_translation_cache(lang):
|
||||
frappe.cache().hdel('lang_user_translations', lang)
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ class User(Document):
|
|||
'frappe.core.doctype.user.user.create_contact',
|
||||
user=self,
|
||||
ignore_mandatory=True,
|
||||
now=frappe.flags.in_test
|
||||
now=frappe.flags.in_test or frappe.flags.in_install
|
||||
)
|
||||
if self.name not in ('Administrator', 'Guest') and not self.user_image:
|
||||
frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ import unittest
|
|||
|
||||
class TestUserPermission(unittest.TestCase):
|
||||
def setUp(self):
|
||||
frappe.db.sql("DELETE FROM `tabUser Permission` WHERE `user`='test_bulk_creation_update@example.com'")
|
||||
frappe.db.sql("""DELETE FROM `tabUser Permission`
|
||||
WHERE `user` in ('test_bulk_creation_update@example.com', 'test_user_perm1@example.com')""")
|
||||
|
||||
def test_default_user_permission_validation(self):
|
||||
user = create_user('test_default_permission@example.com')
|
||||
|
|
@ -20,6 +21,26 @@ class TestUserPermission(unittest.TestCase):
|
|||
param = get_params(user, 'User', perm_user.name, is_default=1)
|
||||
self.assertRaises(frappe.ValidationError, add_user_permissions, param)
|
||||
|
||||
def test_default_user_permission(self):
|
||||
frappe.set_user('Administrator')
|
||||
user = create_user('test_user_perm1@example.com', 'Website Manager')
|
||||
for category in ['general', 'public']:
|
||||
if not frappe.db.exists('Blog Category', category):
|
||||
frappe.get_doc({'doctype': 'Blog Category',
|
||||
'category_name': category, 'title': category}).insert()
|
||||
|
||||
param = get_params(user, 'Blog Category', 'general', is_default=1)
|
||||
add_user_permissions(param)
|
||||
|
||||
param = get_params(user, 'Blog Category', 'public')
|
||||
add_user_permissions(param)
|
||||
|
||||
frappe.set_user('test_user_perm1@example.com')
|
||||
doc = frappe.new_doc("Blog Post")
|
||||
|
||||
self.assertEquals(doc.blog_category, 'general')
|
||||
frappe.set_user('Administrator')
|
||||
|
||||
def test_apply_to_all(self):
|
||||
''' Create User permission for User having access to all applicable Doctypes'''
|
||||
user = create_user('test_bulk_creation_update@example.com')
|
||||
|
|
@ -88,7 +109,7 @@ class TestUserPermission(unittest.TestCase):
|
|||
self.assertIsNone(removed_applicable_second)
|
||||
self.assertEquals(is_created, 1)
|
||||
|
||||
def create_user(email):
|
||||
def create_user(email, role="System Manager"):
|
||||
''' create user with role system manager '''
|
||||
if frappe.db.exists('User', email):
|
||||
return frappe.get_doc('User', email)
|
||||
|
|
@ -96,7 +117,7 @@ def create_user(email):
|
|||
user = frappe.new_doc('User')
|
||||
user.email = email
|
||||
user.first_name = email.split("@")[0]
|
||||
user.add_roles("System Manager")
|
||||
user.add_roles(role)
|
||||
return user
|
||||
|
||||
def get_params(user, doctype, docname, is_default=0, applicable=None):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
.restricted-button {
|
||||
cursor: default;
|
||||
position: relative;
|
||||
right: -5px;
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ frappe.provide('frappe.dashboards.chart_sources');
|
|||
|
||||
|
||||
frappe.pages['dashboard'].on_page_load = function(wrapper) {
|
||||
var page = frappe.ui.make_app_page({
|
||||
frappe.ui.make_app_page({
|
||||
parent: wrapper,
|
||||
title: __("Dashboard"),
|
||||
single_column: true
|
||||
|
|
@ -21,11 +21,18 @@ frappe.pages['dashboard'].on_page_load = function(wrapper) {
|
|||
class Dashboard {
|
||||
constructor(wrapper) {
|
||||
this.wrapper = $(wrapper);
|
||||
$(`<div class="dashboard">
|
||||
$(`<div class="dashboard" style="overflow-y: hidden">
|
||||
<div class="dashboard-graph"></div>
|
||||
</div>`).appendTo(this.wrapper.find(".page-content").empty());
|
||||
this.container = this.wrapper.find(".dashboard-graph");
|
||||
this.page = wrapper.page;
|
||||
|
||||
this.page.set_title_sub(
|
||||
$(`<button class="restricted-button">
|
||||
<span class="octicon octicon-lock"></span>
|
||||
<span>${__('Restricted')}</span>
|
||||
</button>`)
|
||||
);
|
||||
}
|
||||
|
||||
show() {
|
||||
|
|
@ -76,7 +83,16 @@ class Dashboard {
|
|||
}
|
||||
|
||||
refresh() {
|
||||
this.get_permitted_dashboard_charts().then(charts => {
|
||||
frappe.run_serially([
|
||||
() => this.render_cards(),
|
||||
() => this.render_charts()
|
||||
]);
|
||||
}
|
||||
|
||||
render_charts() {
|
||||
return this.get_permitted_items(
|
||||
'frappe.desk.doctype.dashboard.dashboard.get_permitted_charts'
|
||||
).then(charts => {
|
||||
if (!charts.length) {
|
||||
frappe.msgprint(__('No Permitted Charts on this Dashboard'), __('No Permitted Charts'))
|
||||
}
|
||||
|
|
@ -92,6 +108,7 @@ class Dashboard {
|
|||
...chart
|
||||
}
|
||||
});
|
||||
|
||||
this.chart_group = new frappe.widget.WidgetGroup({
|
||||
title: null,
|
||||
container: this.container,
|
||||
|
|
@ -110,14 +127,46 @@ class Dashboard {
|
|||
});
|
||||
}
|
||||
|
||||
get_permitted_dashboard_charts() {
|
||||
render_cards() {
|
||||
return this.get_permitted_items(
|
||||
'frappe.desk.doctype.dashboard.dashboard.get_permitted_cards'
|
||||
).then(cards => {
|
||||
if (!cards.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.number_cards =
|
||||
cards.map(card => {
|
||||
return {
|
||||
name: card.card,
|
||||
};
|
||||
});
|
||||
|
||||
this.number_card_group = new frappe.widget.WidgetGroup({
|
||||
container: this.container,
|
||||
type: "number_card",
|
||||
columns: 3,
|
||||
options: {
|
||||
allow_sorting: false,
|
||||
allow_create: false,
|
||||
allow_delete: false,
|
||||
allow_hiding: false,
|
||||
allow_edit: false,
|
||||
},
|
||||
widgets: this.number_cards,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get_permitted_items(method) {
|
||||
return frappe.xcall(
|
||||
'frappe.desk.doctype.dashboard.dashboard.get_permitted_charts',
|
||||
method,
|
||||
{
|
||||
dashboard_name: this.dashboard_name
|
||||
}).then(charts => {
|
||||
return charts;
|
||||
});
|
||||
}
|
||||
).then(items => {
|
||||
return items;
|
||||
});
|
||||
}
|
||||
|
||||
set_dropdown() {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ frappe.PermissionEngine = Class.extend({
|
|||
setup_page: function() {
|
||||
var me = this;
|
||||
this.doctype_select
|
||||
= this.wrapper.page.add_select(__("Document Types"),
|
||||
= this.wrapper.page.add_select(__("Document Type"),
|
||||
[{value: "", label: __("Select Document Type")+"..."}].concat(this.options.doctypes))
|
||||
.change(function() {
|
||||
frappe.set_route("permission-manager", $(this).val());
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
"column_break_6",
|
||||
"fieldtype",
|
||||
"precision",
|
||||
"show_seconds",
|
||||
"show_days",
|
||||
"options",
|
||||
"fetch_from",
|
||||
"fetch_if_empty",
|
||||
|
|
@ -48,6 +50,7 @@
|
|||
"allow_in_quick_entry",
|
||||
"ignore_xss_filter",
|
||||
"translatable",
|
||||
"hide_border",
|
||||
"description",
|
||||
"permlevel",
|
||||
"width",
|
||||
|
|
@ -55,361 +58,386 @@
|
|||
],
|
||||
"fields": [
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "dt",
|
||||
"fieldtype": "Link",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Document",
|
||||
"oldfieldname": "dt",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "DocType",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
"bold": 1,
|
||||
"fieldname": "dt",
|
||||
"fieldtype": "Link",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Document",
|
||||
"oldfieldname": "dt",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "DocType",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"in_filter": 1,
|
||||
"label": "Label",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "label",
|
||||
"oldfieldtype": "Data"
|
||||
"bold": 1,
|
||||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"in_filter": 1,
|
||||
"label": "Label",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "label",
|
||||
"oldfieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"fieldname": "label_help",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Label Help",
|
||||
"oldfieldtype": "HTML"
|
||||
"fieldname": "label_help",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Label Help",
|
||||
"oldfieldtype": "HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "fieldname",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Fieldname",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "fieldname",
|
||||
"oldfieldtype": "Data",
|
||||
"read_only": 1
|
||||
"fieldname": "fieldname",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Fieldname",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "fieldname",
|
||||
"oldfieldtype": "Data",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"description": "Select the label after which you want to insert new field.",
|
||||
"fieldname": "insert_after",
|
||||
"fieldtype": "Select",
|
||||
"label": "Insert After",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "insert_after",
|
||||
"oldfieldtype": "Select"
|
||||
"description": "Select the label after which you want to insert new field.",
|
||||
"fieldname": "insert_after",
|
||||
"fieldtype": "Select",
|
||||
"label": "Insert After",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "insert_after",
|
||||
"oldfieldtype": "Select"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"default": "Data",
|
||||
"fieldname": "fieldtype",
|
||||
"fieldtype": "Select",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Field Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
|
||||
"reqd": 1
|
||||
"bold": 1,
|
||||
"default": "Data",
|
||||
"fieldname": "fieldtype",
|
||||
"fieldtype": "Select",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Field Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
|
||||
"description": "Set non-standard precision for a Float or Currency field",
|
||||
"fieldname": "precision",
|
||||
"fieldtype": "Select",
|
||||
"label": "Precision",
|
||||
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
|
||||
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
|
||||
"description": "Set non-standard precision for a Float or Currency field",
|
||||
"fieldname": "precision",
|
||||
"fieldtype": "Select",
|
||||
"label": "Precision",
|
||||
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
|
||||
},
|
||||
{
|
||||
"fieldname": "options",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Options",
|
||||
"oldfieldname": "options",
|
||||
"oldfieldtype": "Text"
|
||||
"fieldname": "options",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Options",
|
||||
"oldfieldname": "options",
|
||||
"oldfieldtype": "Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "fetch_from",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Fetch From"
|
||||
"fieldname": "fetch_from",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Fetch From"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
|
||||
"fieldname": "fetch_if_empty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch If Empty"
|
||||
"default": "0",
|
||||
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
|
||||
"fieldname": "fetch_if_empty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch If Empty"
|
||||
},
|
||||
{
|
||||
"fieldname": "options_help",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Options Help",
|
||||
"oldfieldtype": "HTML"
|
||||
"fieldname": "options_help",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Options Help",
|
||||
"oldfieldtype": "HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_11",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldname": "section_break_11",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible",
|
||||
"fieldtype": "Check",
|
||||
"label": "Collapsible"
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible",
|
||||
"fieldtype": "Check",
|
||||
"label": "Collapsible"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Collapsible Depends On"
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Collapsible Depends On"
|
||||
},
|
||||
{
|
||||
"fieldname": "default",
|
||||
"fieldtype": "Text",
|
||||
"label": "Default Value",
|
||||
"oldfieldname": "default",
|
||||
"oldfieldtype": "Text"
|
||||
"fieldname": "default",
|
||||
"fieldtype": "Text",
|
||||
"label": "Default Value",
|
||||
"oldfieldname": "default",
|
||||
"oldfieldtype": "Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Depends On",
|
||||
"length": 255
|
||||
"fieldname": "depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Depends On",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text",
|
||||
"label": "Field Description",
|
||||
"oldfieldname": "description",
|
||||
"oldfieldtype": "Text",
|
||||
"print_width": "300px",
|
||||
"width": "300px"
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text",
|
||||
"label": "Field Description",
|
||||
"oldfieldname": "description",
|
||||
"oldfieldtype": "Text",
|
||||
"print_width": "300px",
|
||||
"width": "300px"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "permlevel",
|
||||
"fieldtype": "Int",
|
||||
"label": "Permission Level",
|
||||
"oldfieldname": "permlevel",
|
||||
"oldfieldtype": "Int"
|
||||
"default": "0",
|
||||
"fieldname": "permlevel",
|
||||
"fieldtype": "Int",
|
||||
"label": "Permission Level",
|
||||
"oldfieldname": "permlevel",
|
||||
"oldfieldtype": "Int"
|
||||
},
|
||||
{
|
||||
"fieldname": "width",
|
||||
"fieldtype": "Data",
|
||||
"label": "Width",
|
||||
"oldfieldname": "width",
|
||||
"oldfieldtype": "Data"
|
||||
"fieldname": "width",
|
||||
"fieldtype": "Data",
|
||||
"label": "Width",
|
||||
"oldfieldname": "width",
|
||||
"oldfieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
|
||||
"fieldname": "columns",
|
||||
"fieldtype": "Int",
|
||||
"label": "Columns"
|
||||
"description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
|
||||
"fieldname": "columns",
|
||||
"fieldtype": "Int",
|
||||
"label": "Columns"
|
||||
},
|
||||
{
|
||||
"fieldname": "properties",
|
||||
"fieldtype": "Column Break",
|
||||
"oldfieldtype": "Column Break",
|
||||
"print_width": "50%",
|
||||
"width": "50%"
|
||||
"fieldname": "properties",
|
||||
"fieldtype": "Column Break",
|
||||
"oldfieldtype": "Column Break",
|
||||
"print_width": "50%",
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "reqd",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Is Mandatory Field",
|
||||
"oldfieldname": "reqd",
|
||||
"oldfieldtype": "Check"
|
||||
"default": "0",
|
||||
"fieldname": "reqd",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Is Mandatory Field",
|
||||
"oldfieldname": "reqd",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "unique",
|
||||
"fieldtype": "Check",
|
||||
"label": "Unique"
|
||||
"default": "0",
|
||||
"fieldname": "unique",
|
||||
"fieldtype": "Check",
|
||||
"label": "Unique"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "read_only",
|
||||
"fieldtype": "Check",
|
||||
"label": "Read Only"
|
||||
"default": "0",
|
||||
"fieldname": "read_only",
|
||||
"fieldtype": "Check",
|
||||
"label": "Read Only"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype===\"Link\"",
|
||||
"fieldname": "ignore_user_permissions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore User Permissions"
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype===\"Link\"",
|
||||
"fieldname": "ignore_user_permissions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore User Permissions"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "hidden",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hidden"
|
||||
"default": "0",
|
||||
"fieldname": "hidden",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hidden"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "print_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide",
|
||||
"oldfieldname": "print_hide",
|
||||
"oldfieldtype": "Check"
|
||||
"default": "0",
|
||||
"fieldname": "print_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide",
|
||||
"oldfieldname": "print_hide",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
|
||||
"fieldname": "print_hide_if_no_value",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide If No Value"
|
||||
"default": "0",
|
||||
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
|
||||
"fieldname": "print_hide_if_no_value",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide If No Value"
|
||||
},
|
||||
{
|
||||
"fieldname": "print_width",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Print Width",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
"fieldname": "print_width",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Print Width",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "no_copy",
|
||||
"fieldtype": "Check",
|
||||
"label": "No Copy",
|
||||
"oldfieldname": "no_copy",
|
||||
"oldfieldtype": "Check"
|
||||
"default": "0",
|
||||
"fieldname": "no_copy",
|
||||
"fieldtype": "Check",
|
||||
"label": "No Copy",
|
||||
"oldfieldname": "no_copy",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_on_submit",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow on Submit",
|
||||
"oldfieldname": "allow_on_submit",
|
||||
"oldfieldtype": "Check"
|
||||
"default": "0",
|
||||
"fieldname": "allow_on_submit",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow on Submit",
|
||||
"oldfieldname": "allow_on_submit",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_list_view",
|
||||
"fieldtype": "Check",
|
||||
"label": "In List View"
|
||||
"default": "0",
|
||||
"fieldname": "in_list_view",
|
||||
"fieldtype": "Check",
|
||||
"label": "In List View"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_standard_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Standard Filter"
|
||||
"default": "0",
|
||||
"fieldname": "in_standard_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Standard Filter"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
|
||||
"fieldname": "in_global_search",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Global Search"
|
||||
"default": "0",
|
||||
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
|
||||
"fieldname": "in_global_search",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Global Search"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "bold",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bold"
|
||||
"default": "0",
|
||||
"fieldname": "bold",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bold"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "report_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Report Hide",
|
||||
"oldfieldname": "report_hide",
|
||||
"oldfieldtype": "Check"
|
||||
"default": "0",
|
||||
"fieldname": "report_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Report Hide",
|
||||
"oldfieldname": "report_hide",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "search_index",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Index",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
"default": "0",
|
||||
"fieldname": "search_index",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Index",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
|
||||
"fieldname": "ignore_xss_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore XSS Filter"
|
||||
"default": "0",
|
||||
"description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
|
||||
"fieldname": "ignore_xss_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore XSS Filter"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
|
||||
"fieldname": "translatable",
|
||||
"fieldtype": "Check",
|
||||
"label": "Translatable"
|
||||
"default": "1",
|
||||
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
|
||||
"fieldname": "translatable",
|
||||
"fieldtype": "Check",
|
||||
"label": "Translatable"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
|
||||
"fieldname": "length",
|
||||
"fieldtype": "Int",
|
||||
"label": "Length"
|
||||
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
|
||||
"fieldname": "length",
|
||||
"fieldtype": "Int",
|
||||
"label": "Length"
|
||||
},
|
||||
{
|
||||
"fieldname": "mandatory_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Mandatory Depends On",
|
||||
"length": 255
|
||||
"fieldname": "mandatory_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Mandatory Depends On",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"fieldname": "read_only_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Read Only Depends On",
|
||||
"length": 255
|
||||
"fieldname": "read_only_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Read Only Depends On",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_in_quick_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow in Quick Entry"
|
||||
"default": "0",
|
||||
"fieldname": "allow_in_quick_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow in Quick Entry"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_preview",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Preview"
|
||||
"default": "0",
|
||||
"fieldname": "in_preview",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Preview"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:doc.fieldtype === \"Duration\";",
|
||||
"fieldname": "show_seconds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Seconds",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:doc.fieldtype === \"Duration\";",
|
||||
"fieldname": "show_days",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Days",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Section Break'",
|
||||
"fieldname": "hide_border",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Border"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-glass",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-10 11:57:10.392218",
|
||||
"modified": "2020-05-15 23:43:00.123572",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Custom Field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Administrator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Administrator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "dt,label,fieldtype,options",
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ class CustomField(Document):
|
|||
if not self.fieldname:
|
||||
frappe.throw(_("Fieldname not set for Custom Field"))
|
||||
|
||||
if self.fieldname in fieldnames:
|
||||
frappe.throw(_("A field with the name '{}' already exists in doctype {}.").format(self.fieldname, self.dt))
|
||||
|
||||
if self.get('translatable', 0) and not supports_translation(self.fieldtype):
|
||||
self.translatable = 0
|
||||
|
||||
|
|
|
|||
20
frappe/custom/doctype/custom_link/custom_link.js
Normal file
20
frappe/custom/doctype/custom_link/custom_link.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) 2020, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Custom Link', {
|
||||
refresh: function(frm) {
|
||||
frm.set_query("document_type", function () {
|
||||
return {
|
||||
filters: {
|
||||
custom: 0,
|
||||
istable: 0,
|
||||
module: ['not in', ["Email", "Core", "Custom", "Event Streaming", "Social", "Data Migration", "Geo", "Desk"]]
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.add_custom_button(__('Go to {0} List', [frm.doc.document_type]), function() {
|
||||
frappe.set_route('List', frm.doc.document_type);
|
||||
});
|
||||
}
|
||||
});
|
||||
52
frappe/custom/doctype/custom_link/custom_link.json
Normal file
52
frappe/custom/doctype/custom_link/custom_link.json
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "field:document_type",
|
||||
"creation": "2020-04-08 15:16:44.342509",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"document_type",
|
||||
"links"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "document_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Document Type",
|
||||
"options": "DocType",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "links",
|
||||
"fieldtype": "Table",
|
||||
"label": "Links",
|
||||
"options": "DocType Link"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-04-08 16:42:59.402671",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Custom Link",
|
||||
"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
|
||||
}
|
||||
|
|
@ -6,5 +6,5 @@ from __future__ import unicode_literals
|
|||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class CSSClass(Document):
|
||||
class CustomLink(Document):
|
||||
pass
|
||||
|
|
@ -6,5 +6,5 @@ from __future__ import unicode_literals
|
|||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestCSSClass(unittest.TestCase):
|
||||
class TestCustomLink(unittest.TestCase):
|
||||
pass
|
||||
|
|
@ -76,7 +76,8 @@ docfield_properties = {
|
|||
'remember_last_selected_value': 'Check',
|
||||
'allow_bulk_edit': 'Check',
|
||||
'auto_repeat': 'Link',
|
||||
'allow_in_quick_entry': 'Check'
|
||||
'allow_in_quick_entry': 'Check',
|
||||
'hide_border': 'Check'
|
||||
}
|
||||
|
||||
allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'),
|
||||
|
|
@ -118,7 +119,7 @@ class CustomizeForm(Document):
|
|||
|
||||
# load custom translation
|
||||
translation = self.get_name_translation()
|
||||
self.label = translation.target_name if translation else ''
|
||||
self.label = translation.translated_text if translation else ''
|
||||
|
||||
#If allow_auto_repeat is set, add auto_repeat custom field.
|
||||
if self.allow_auto_repeat:
|
||||
|
|
@ -131,16 +132,17 @@ class CustomizeForm(Document):
|
|||
|
||||
def get_name_translation(self):
|
||||
'''Get translation object if exists of current doctype name in the default language'''
|
||||
return frappe.get_value('Translation',
|
||||
{'source_name': self.doc_type, 'language': frappe.local.lang or 'en'},
|
||||
['name', 'target_name'], as_dict=True)
|
||||
return frappe.get_value('Translation', {
|
||||
'source_text': self.doc_type,
|
||||
'language': frappe.local.lang or 'en'
|
||||
}, ['name', 'translated_text'], as_dict=True)
|
||||
|
||||
def set_name_translation(self):
|
||||
'''Create, update custom translation for this doctype'''
|
||||
current = self.get_name_translation()
|
||||
if current:
|
||||
if self.label and current.target_name != self.label:
|
||||
frappe.db.set_value('Translation', current.name, 'target_name', self.label)
|
||||
if self.label and current.translated_text != self.label:
|
||||
frappe.db.set_value('Translation', current.name, 'translated_text', self.label)
|
||||
frappe.translate.clear_cache()
|
||||
else:
|
||||
# clear translation
|
||||
|
|
@ -149,8 +151,8 @@ class CustomizeForm(Document):
|
|||
else:
|
||||
if self.label:
|
||||
frappe.get_doc(dict(doctype='Translation',
|
||||
source_name=self.doc_type,
|
||||
target_name=self.label,
|
||||
source_text=self.doc_type,
|
||||
translated_text=self.label,
|
||||
language_code=frappe.local.lang or 'en')).insert()
|
||||
|
||||
def clear_existing_doc(self):
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@
|
|||
"label",
|
||||
"fieldtype",
|
||||
"fieldname",
|
||||
"show_seconds",
|
||||
"show_days",
|
||||
"reqd",
|
||||
"unique",
|
||||
"in_list_view",
|
||||
|
|
@ -39,6 +41,7 @@
|
|||
"allow_on_submit",
|
||||
"report_hide",
|
||||
"remember_last_selected_value",
|
||||
"hide_border",
|
||||
"property_depends_on_section",
|
||||
"mandatory_depends_on",
|
||||
"column_break_33",
|
||||
|
|
@ -57,343 +60,368 @@
|
|||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "label_and_type",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Label and Type"
|
||||
"fieldname": "label_and_type",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Label and Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Label",
|
||||
"oldfieldname": "label",
|
||||
"oldfieldtype": "Data",
|
||||
"search_index": 1
|
||||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Label",
|
||||
"oldfieldname": "label",
|
||||
"oldfieldtype": "Data",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "Data",
|
||||
"fieldname": "fieldtype",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
"default": "Data",
|
||||
"fieldname": "fieldtype",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "fieldname",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Name",
|
||||
"oldfieldname": "fieldname",
|
||||
"oldfieldtype": "Data",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
"fieldname": "fieldname",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Name",
|
||||
"oldfieldname": "fieldname",
|
||||
"oldfieldtype": "Data",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
|
||||
"fieldname": "reqd",
|
||||
"fieldtype": "Check",
|
||||
"label": "Mandatory",
|
||||
"oldfieldname": "reqd",
|
||||
"oldfieldtype": "Check",
|
||||
"print_width": "50px",
|
||||
"width": "50px"
|
||||
"default": "0",
|
||||
"depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
|
||||
"fieldname": "reqd",
|
||||
"fieldtype": "Check",
|
||||
"label": "Mandatory",
|
||||
"oldfieldname": "reqd",
|
||||
"oldfieldtype": "Check",
|
||||
"print_width": "50px",
|
||||
"width": "50px"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "unique",
|
||||
"fieldtype": "Check",
|
||||
"label": "Unique"
|
||||
"default": "0",
|
||||
"fieldname": "unique",
|
||||
"fieldtype": "Check",
|
||||
"label": "Unique"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_list_view",
|
||||
"fieldtype": "Check",
|
||||
"label": "In List View"
|
||||
"default": "0",
|
||||
"fieldname": "in_list_view",
|
||||
"fieldtype": "Check",
|
||||
"label": "In List View"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_standard_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Standard Filter"
|
||||
"default": "0",
|
||||
"fieldname": "in_standard_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Standard Filter"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
|
||||
"fieldname": "in_global_search",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Global Search"
|
||||
"default": "0",
|
||||
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
|
||||
"fieldname": "in_global_search",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Global Search"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "bold",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bold"
|
||||
"default": "0",
|
||||
"fieldname": "bold",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bold"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
|
||||
"fieldname": "translatable",
|
||||
"fieldtype": "Check",
|
||||
"label": "Translatable"
|
||||
"default": "1",
|
||||
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
|
||||
"fieldname": "translatable",
|
||||
"fieldtype": "Check",
|
||||
"label": "Translatable"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_7",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldname": "column_break_7",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
|
||||
"description": "Set non-standard precision for a Float or Currency field",
|
||||
"fieldname": "precision",
|
||||
"fieldtype": "Select",
|
||||
"label": "Precision",
|
||||
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
|
||||
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
|
||||
"description": "Set non-standard precision for a Float or Currency field",
|
||||
"fieldname": "precision",
|
||||
"fieldtype": "Select",
|
||||
"label": "Precision",
|
||||
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'], doc.fieldtype)",
|
||||
"fieldname": "length",
|
||||
"fieldtype": "Int",
|
||||
"label": "Length"
|
||||
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'], doc.fieldtype)",
|
||||
"fieldname": "length",
|
||||
"fieldtype": "Int",
|
||||
"label": "Length"
|
||||
},
|
||||
{
|
||||
"description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.",
|
||||
"fieldname": "options",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Options",
|
||||
"oldfieldname": "options",
|
||||
"oldfieldtype": "Text"
|
||||
"description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.",
|
||||
"fieldname": "options",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Options",
|
||||
"oldfieldname": "options",
|
||||
"oldfieldtype": "Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "fetch_from",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Fetch From"
|
||||
"fieldname": "fetch_from",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Fetch From"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
|
||||
"fieldname": "fetch_if_empty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch If Empty"
|
||||
"default": "0",
|
||||
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
|
||||
"fieldname": "fetch_if_empty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch If Empty"
|
||||
},
|
||||
{
|
||||
"fieldname": "permissions",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Permissions"
|
||||
"fieldname": "permissions",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Permissions"
|
||||
},
|
||||
{
|
||||
"description": "This field will appear only if the fieldname defined here has value OR the rules are true (examples): \nmyfield\neval:doc.myfield=='My Value'\neval:doc.age>18",
|
||||
"fieldname": "depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Depends On",
|
||||
"oldfieldname": "depends_on",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "JS"
|
||||
"description": "This field will appear only if the fieldname defined here has value OR the rules are true (examples): \nmyfield\neval:doc.myfield=='My Value'\neval:doc.age>18",
|
||||
"fieldname": "depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Depends On",
|
||||
"oldfieldname": "depends_on",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "JS"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "permlevel",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Perm Level",
|
||||
"oldfieldname": "permlevel",
|
||||
"oldfieldtype": "Int"
|
||||
"default": "0",
|
||||
"fieldname": "permlevel",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Perm Level",
|
||||
"oldfieldname": "permlevel",
|
||||
"oldfieldtype": "Int"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "hidden",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hidden",
|
||||
"oldfieldname": "hidden",
|
||||
"oldfieldtype": "Check",
|
||||
"print_width": "50px",
|
||||
"width": "50px"
|
||||
"default": "0",
|
||||
"fieldname": "hidden",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hidden",
|
||||
"oldfieldname": "hidden",
|
||||
"oldfieldtype": "Check",
|
||||
"print_width": "50px",
|
||||
"width": "50px"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "read_only",
|
||||
"fieldtype": "Check",
|
||||
"label": "Read Only"
|
||||
"default": "0",
|
||||
"fieldname": "read_only",
|
||||
"fieldtype": "Check",
|
||||
"label": "Read Only"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible",
|
||||
"fieldtype": "Check",
|
||||
"label": "Collapsible"
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible",
|
||||
"fieldtype": "Check",
|
||||
"label": "Collapsible"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.fieldtype == \"Table\"",
|
||||
"fieldname": "allow_bulk_edit",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Bulk Edit"
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.fieldtype == \"Table\"",
|
||||
"fieldname": "allow_bulk_edit",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Bulk Edit"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Collapsible Depends On",
|
||||
"options": "JS"
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Collapsible Depends On",
|
||||
"options": "JS"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_14",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldname": "column_break_14",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_user_permissions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore User Permissions"
|
||||
"default": "0",
|
||||
"fieldname": "ignore_user_permissions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore User Permissions"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_on_submit",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow on Submit",
|
||||
"oldfieldname": "allow_on_submit",
|
||||
"oldfieldtype": "Check"
|
||||
"default": "0",
|
||||
"fieldname": "allow_on_submit",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow on Submit",
|
||||
"oldfieldname": "allow_on_submit",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "report_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Report Hide",
|
||||
"oldfieldname": "report_hide",
|
||||
"oldfieldtype": "Check"
|
||||
"default": "0",
|
||||
"fieldname": "report_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Report Hide",
|
||||
"oldfieldname": "report_hide",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:(doc.fieldtype == 'Link')",
|
||||
"fieldname": "remember_last_selected_value",
|
||||
"fieldtype": "Check",
|
||||
"label": "Remember Last Selected Value"
|
||||
"default": "0",
|
||||
"depends_on": "eval:(doc.fieldtype == 'Link')",
|
||||
"fieldname": "remember_last_selected_value",
|
||||
"fieldtype": "Check",
|
||||
"label": "Remember Last Selected Value"
|
||||
},
|
||||
{
|
||||
"fieldname": "display",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Display"
|
||||
"fieldname": "display",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Display"
|
||||
},
|
||||
{
|
||||
"fieldname": "default",
|
||||
"fieldtype": "Text",
|
||||
"label": "Default",
|
||||
"oldfieldname": "default",
|
||||
"oldfieldtype": "Text"
|
||||
"fieldname": "default",
|
||||
"fieldtype": "Text",
|
||||
"label": "Default",
|
||||
"oldfieldname": "default",
|
||||
"oldfieldtype": "Text"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Filter",
|
||||
"oldfieldname": "in_filter",
|
||||
"oldfieldtype": "Check",
|
||||
"print_width": "50px",
|
||||
"width": "50px"
|
||||
"default": "0",
|
||||
"fieldname": "in_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Filter",
|
||||
"oldfieldname": "in_filter",
|
||||
"oldfieldtype": "Check",
|
||||
"print_width": "50px",
|
||||
"width": "50px"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_21",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldname": "column_break_21",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text",
|
||||
"label": "Description",
|
||||
"oldfieldname": "description",
|
||||
"oldfieldtype": "Text",
|
||||
"print_width": "300px",
|
||||
"width": "300px"
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text",
|
||||
"label": "Description",
|
||||
"oldfieldname": "description",
|
||||
"oldfieldtype": "Text",
|
||||
"print_width": "300px",
|
||||
"width": "300px"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "print_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide",
|
||||
"oldfieldname": "print_hide",
|
||||
"oldfieldtype": "Check"
|
||||
"default": "0",
|
||||
"fieldname": "print_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide",
|
||||
"oldfieldname": "print_hide",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
|
||||
"fieldname": "print_hide_if_no_value",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide If No Value"
|
||||
"default": "0",
|
||||
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
|
||||
"fieldname": "print_hide_if_no_value",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide If No Value"
|
||||
},
|
||||
{
|
||||
"description": "Print Width of the field, if the field is a column in a table",
|
||||
"fieldname": "print_width",
|
||||
"fieldtype": "Data",
|
||||
"label": "Print Width",
|
||||
"print_width": "50px",
|
||||
"width": "50px"
|
||||
"description": "Print Width of the field, if the field is a column in a table",
|
||||
"fieldname": "print_width",
|
||||
"fieldtype": "Data",
|
||||
"label": "Print Width",
|
||||
"print_width": "50px",
|
||||
"width": "50px"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:cur_frm.doc.istable",
|
||||
"description": "Number of columns for a field in a Grid (Total Columns in a grid should be less than 11)",
|
||||
"fieldname": "columns",
|
||||
"fieldtype": "Int",
|
||||
"label": "Columns"
|
||||
"depends_on": "eval:cur_frm.doc.istable",
|
||||
"description": "Number of columns for a field in a Grid (Total Columns in a grid should be less than 11)",
|
||||
"fieldname": "columns",
|
||||
"fieldtype": "Int",
|
||||
"label": "Columns"
|
||||
},
|
||||
{
|
||||
"fieldname": "width",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Width",
|
||||
"oldfieldname": "width",
|
||||
"oldfieldtype": "Data",
|
||||
"print_width": "50px",
|
||||
"width": "50px"
|
||||
"fieldname": "width",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Width",
|
||||
"oldfieldname": "width",
|
||||
"oldfieldtype": "Data",
|
||||
"print_width": "50px",
|
||||
"width": "50px"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_custom_field",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Is Custom Field",
|
||||
"read_only": 1
|
||||
"default": "0",
|
||||
"fieldname": "is_custom_field",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Is Custom Field",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_in_quick_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow in Quick Entry"
|
||||
"default": "0",
|
||||
"fieldname": "allow_in_quick_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow in Quick Entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "property_depends_on_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Property Depends On"
|
||||
"fieldname": "property_depends_on_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Property Depends On"
|
||||
},
|
||||
{
|
||||
"fieldname": "mandatory_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Mandatory Depends On",
|
||||
"options": "JS"
|
||||
"fieldname": "mandatory_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Mandatory Depends On",
|
||||
"options": "JS"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_33",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldname": "column_break_33",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "read_only_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Read Only Depends On",
|
||||
"options": "JS"
|
||||
"fieldname": "read_only_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Read Only Depends On",
|
||||
"options": "JS"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_preview",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Preview"
|
||||
"default": "0",
|
||||
"fieldname": "in_preview",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Preview"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:doc.fieldtype === \"Duration\";",
|
||||
"fieldname": "show_seconds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Seconds",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:doc.fieldtype === \"Duration\";",
|
||||
"fieldname": "show_days",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Days",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Section Break'",
|
||||
"fieldname": "hide_border",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Border"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-10 11:58:44.573537",
|
||||
"modified": "2020-05-15 23:45:46.810869",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form Field",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2020-05-14 16:45:47.196395",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"document_type",
|
||||
"column_break_2",
|
||||
"attachments",
|
||||
"overwrite",
|
||||
"section_break_4",
|
||||
"filters_json"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "document_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Document Type",
|
||||
"options": "DocType",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "attachments",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Include Attachments"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "overwrite",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Overwrite"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_4",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "filters_json",
|
||||
"fieldtype": "Code",
|
||||
"label": "Filters",
|
||||
"options": "JSON"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-05-14 16:45:47.196395",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Package Document Type",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class OnboardingSlideHelpLink(Document):
|
||||
class PackageDocumentType(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2020-05-13 16:04:32.724663",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"instance_url",
|
||||
"username",
|
||||
"password"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "instance_url",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Site URL",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "username",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Username",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "password",
|
||||
"fieldtype": "Password",
|
||||
"in_list_view": 1,
|
||||
"label": "Password",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-05-15 17:35:16.282235",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Package Publish Target",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class PackagePublishTarget(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
// Copyright (c) 2020, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Package Publish Tool', {
|
||||
refresh: function(frm) {
|
||||
frm.set_query("document_type", "package_details", function () {
|
||||
return {
|
||||
filters: {
|
||||
"istable": 0,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frappe.realtime.on("package", (data) => {
|
||||
frm.dashboard.show_progress(data.prefix, data.progress / data.total * 100, __("{0}", [data.message]));
|
||||
if ((data.progress+1) != data.total) {
|
||||
frm.dashboard.show_progress(data.prefix, data.progress / data.total * 100, __("{0}", [data.message]));
|
||||
} else {
|
||||
frm.dashboard.hide_progress();
|
||||
}
|
||||
});
|
||||
|
||||
frm.trigger("show_instructions");
|
||||
frm.trigger("last_deployed_on");
|
||||
frm.trigger("set_dirty_trigger");
|
||||
frm.trigger("set_deploy_primary_action");
|
||||
},
|
||||
last_deployed_on: function(frm) {
|
||||
if (frm.doc.last_deployed_on) {
|
||||
frm.trigger("show_indicator");
|
||||
}
|
||||
},
|
||||
show_indicator: function(frm) {
|
||||
let pretty_date = frappe.datetime.prettyDate(frm.doc.last_deployed_on);
|
||||
frm.page.set_indicator(__("Last published {0}", [pretty_date]), "blue");
|
||||
},
|
||||
set_dirty_trigger: function(frm) {
|
||||
$(frm.wrapper).on("dirty", function() {
|
||||
frm.page.set_primary_action(__('Save'), () => frm.save());
|
||||
});
|
||||
},
|
||||
set_deploy_primary_action: function(frm) {
|
||||
if (frm.doc.package_details.length && frm.doc.instances.length) {
|
||||
frm.page.set_primary_action(__("Publish"), function () {
|
||||
frappe.show_alert({
|
||||
message: __("Publishing documents..."),
|
||||
indicator: "green"
|
||||
});
|
||||
|
||||
frappe.call({
|
||||
method: "frappe.custom.doctype.package_publish_tool.package_publish_tool.deploy_package",
|
||||
callback: function() {
|
||||
frm.reload_doc();
|
||||
frappe.msgprint(__("Documents have been published."));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
show_instructions: function(frm) {
|
||||
let field = frm.get_field("html_info");
|
||||
field.html(`
|
||||
<p class="text-muted text-medium">
|
||||
Package Publish Tool let's you copy documents from your site to any other remote site.
|
||||
Follow the steps below to publish.
|
||||
</p>
|
||||
<ol class="text-muted small">
|
||||
<li>Add Document Types that you want to copy from the table below. You can also add filters by expanding the row.</li>
|
||||
<li>Add the Sites URL where you want to copy these documents, and enter the Username and Password.</li>
|
||||
<li>Click on Save. Now, you can click on Publish and the documents will be copied.</li>
|
||||
</ol>
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
frappe.ui.form.on('Package Document Type', {
|
||||
form_render: function (frm, cdt, cdn) {
|
||||
function _show_filters(filters, table) {
|
||||
table.find('tbody').empty();
|
||||
|
||||
if (filters.length > 0) {
|
||||
filters.forEach(filter => {
|
||||
const filter_row =
|
||||
$(`<tr>
|
||||
<td>${filter[1]}</td>
|
||||
<td>${filter[2] || ""}</td>
|
||||
<td>${filter[3]}</td>
|
||||
</tr>`);
|
||||
|
||||
table.find('tbody').append(filter_row);
|
||||
});
|
||||
} else {
|
||||
const filter_row = $(`<tr><td colspan="3" class="text-muted text-center">
|
||||
${__("Click to Set Filters")}</td></tr>`);
|
||||
table.find('tbody').append(filter_row);
|
||||
}
|
||||
}
|
||||
|
||||
let row = frappe.get_doc(cdt, cdn);
|
||||
|
||||
let wrapper = $(`[data-fieldname="filters_json"]`).empty();
|
||||
let table = $(`<table class="table table-bordered" style="cursor:pointer; margin:0px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 33%">${__('Filter')}</th>
|
||||
<th style="width: 33%">${__('Condition')}</th>
|
||||
<th>${__('Value')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>`).appendTo(wrapper);
|
||||
$(`<p class="text-muted small">${__("Click table to edit")}</p>`).appendTo(wrapper);
|
||||
|
||||
let filters = JSON.parse(row.filters_json || '[]');
|
||||
_show_filters(filters, table);
|
||||
|
||||
table.on('click', () => {
|
||||
if (!row.document_type) {
|
||||
frappe.msgprint(__("Select Document Type."));
|
||||
return;
|
||||
}
|
||||
|
||||
frappe.model.with_doctype(row.document_type, function() {
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __('Set Filters'),
|
||||
fields: [
|
||||
{
|
||||
fieldtype: 'HTML',
|
||||
label: 'Filters',
|
||||
fieldname: 'filter_area',
|
||||
}
|
||||
],
|
||||
primary_action: function() {
|
||||
let values = filter_group.get_filters();
|
||||
let flt = [];
|
||||
if (values) {
|
||||
values.forEach(function(value) {
|
||||
flt.push([value[0], value[1], value[2], value[3]]);
|
||||
});
|
||||
}
|
||||
row.filters_json = JSON.stringify(flt);
|
||||
_show_filters(flt, table);
|
||||
dialog.hide();
|
||||
},
|
||||
primary_action_label: "Set"
|
||||
});
|
||||
|
||||
let filter_group = new frappe.ui.FilterGroup({
|
||||
parent: dialog.get_field('filter_area').$wrapper,
|
||||
doctype: row.document_type,
|
||||
on_change: () => {},
|
||||
});
|
||||
filter_group.add_filters_to_filter_group(filters);
|
||||
dialog.show();
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2020-05-13 15:54:38.082657",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"html_info",
|
||||
"sb_00",
|
||||
"package_details",
|
||||
"sb_01",
|
||||
"instances",
|
||||
"last_deployed_on"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"description": "Click on the row for accessing filters.",
|
||||
"fieldname": "package_details",
|
||||
"fieldtype": "Table",
|
||||
"label": "Document Types",
|
||||
"options": "Package Document Type",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "instances",
|
||||
"fieldtype": "Table",
|
||||
"label": "Sites",
|
||||
"options": "Package Publish Target",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "html_info",
|
||||
"fieldtype": "HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "last_deployed_on",
|
||||
"fieldtype": "Datetime",
|
||||
"hidden": 1,
|
||||
"label": "Last Deployed On",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sb_00",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "sb_01",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-05-15 17:31:37.060199",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Package Publish Tool",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "All",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import json
|
||||
import datetime
|
||||
import base64
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.file_manager import save_file, get_file
|
||||
from frappe import _
|
||||
from six import string_types
|
||||
from frappe.frappeclient import FrappeClient
|
||||
from frappe.utils import get_datetime_str, get_datetime
|
||||
from frappe.utils.password import get_decrypted_password
|
||||
|
||||
class PackagePublishTool(Document):
|
||||
pass
|
||||
|
||||
@frappe.whitelist()
|
||||
def deploy_package():
|
||||
package, doc = export_package()
|
||||
|
||||
file_name = "Package-" + get_datetime_str(get_datetime())
|
||||
|
||||
length = len(doc.instances)
|
||||
for idx, instance in enumerate(doc.instances):
|
||||
frappe.publish_realtime("package", {"progress": idx, "total": length, "message": instance.instance_url, "prefix": _("Deploying")},
|
||||
user=frappe.session.user)
|
||||
|
||||
install_package_to_remote(package, instance)
|
||||
|
||||
frappe.db.set_value("Package Publish Tool", "Package Publish Tool", "last_deployed_on", frappe.utils.now_datetime())
|
||||
|
||||
def install_package_to_remote(package, instance):
|
||||
try:
|
||||
connection = FrappeClient(instance.instance_url, instance.username, get_decrypted_password(instance.doctype, instance.name))
|
||||
except Exception:
|
||||
frappe.log_error(frappe.get_traceback())
|
||||
frappe.throw(_("Couldn't connect to site {0}. Please check Error Logs.").format(instance.instance_url))
|
||||
|
||||
try:
|
||||
connection.post_request({
|
||||
"cmd": "frappe.custom.doctype.package_publish_tool.package_publish_tool.import_package",
|
||||
"package": json.dumps(package)
|
||||
})
|
||||
except Exception:
|
||||
frappe.log_error(frappe.get_traceback())
|
||||
frappe.throw(_("Error while installing package to site {0}. Please check Error Logs.").format(instance.instance_url))
|
||||
|
||||
@frappe.whitelist()
|
||||
def export_package():
|
||||
"""Export package as JSON."""
|
||||
package_doc = frappe.get_single("Package Publish Tool")
|
||||
package = []
|
||||
|
||||
for doctype in package_doc.package_details:
|
||||
filters = []
|
||||
|
||||
if doctype.get("filters_json"):
|
||||
filters = json.loads(doctype.get("filters_json"))
|
||||
|
||||
docs = frappe.get_all(doctype.get("document_type"), filters=filters)
|
||||
length = len(docs)
|
||||
|
||||
for idx, doc in enumerate(docs):
|
||||
frappe.publish_realtime("package", {
|
||||
"progress":idx, "total":length,
|
||||
"message":doctype.get("document_type"),
|
||||
"prefix": _("Exporting")
|
||||
},
|
||||
user=frappe.session.user)
|
||||
|
||||
document = frappe.get_doc(doctype.get("document_type"), doc.name).as_dict()
|
||||
attachments = []
|
||||
|
||||
if doctype.attachments:
|
||||
filters = {
|
||||
"attached_to_doctype": document.get("doctype"),
|
||||
"attached_to_name": document.get("name")
|
||||
}
|
||||
|
||||
for f in frappe.get_list("File", filters=filters):
|
||||
fname, fcontents = get_file(f.name)
|
||||
attachments.append({
|
||||
"fname": fname,
|
||||
"content": base64.b64encode(fcontents).decode('ascii')
|
||||
})
|
||||
|
||||
document.update({
|
||||
"__attachments": attachments,
|
||||
"__overwrite": True if doctype.overwrite else False
|
||||
})
|
||||
|
||||
package.append(document)
|
||||
|
||||
return post_process(package), package_doc
|
||||
|
||||
@frappe.whitelist()
|
||||
def import_package(package=None):
|
||||
"""Import package from JSON."""
|
||||
if isinstance(package, string_types):
|
||||
package = json.loads(package)
|
||||
|
||||
for doc in package:
|
||||
modified = doc.pop("modified")
|
||||
overwrite = doc.pop("__overwrite")
|
||||
attachments = doc.pop("__attachments")
|
||||
exists = frappe.db.exists(doc.get("doctype"), doc.get("name"))
|
||||
|
||||
if not exists:
|
||||
d = frappe.get_doc(doc).insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||
if attachments:
|
||||
add_attachment(attachments, d)
|
||||
else:
|
||||
docname = doc.pop("name")
|
||||
document = frappe.get_doc(doc.get("doctype"), docname)
|
||||
|
||||
if overwrite:
|
||||
update_document(document, doc, attachments)
|
||||
|
||||
else:
|
||||
if frappe.utils.get_datetime(document.modified) < frappe.utils.get_datetime(modified):
|
||||
update_document(document, doc, attachments)
|
||||
|
||||
def update_document(document, doc, attachments):
|
||||
document.update(doc)
|
||||
document.save()
|
||||
if attachments:
|
||||
add_attachment(attachments, document)
|
||||
|
||||
def add_attachment(attachments, doc):
|
||||
for attachment in attachments:
|
||||
save_file(attachment.get("fname"), base64.b64decode(attachment.get("content")), doc.get("doctype"), doc.get("name"))
|
||||
|
||||
def post_process(package):
|
||||
"""Remove the keys from Document and Child Document. Convert datetime, date, time to str."""
|
||||
del_keys = ('modified_by', 'creation', 'owner', 'idx', 'docstatus')
|
||||
child_del_keys = ('modified_by', 'creation', 'owner', 'idx', 'docstatus', 'name')
|
||||
|
||||
for doc in package:
|
||||
for key in del_keys:
|
||||
if key in doc:
|
||||
del doc[key]
|
||||
|
||||
for key, value in doc.items():
|
||||
stringified_value = get_stringified_value(value)
|
||||
if stringified_value:
|
||||
doc[key] = stringified_value
|
||||
|
||||
if not isinstance(value, list):
|
||||
continue
|
||||
|
||||
for child in value:
|
||||
for child_key in child_del_keys:
|
||||
if child_key in child:
|
||||
del child[child_key]
|
||||
|
||||
for child_key, child_value in child.items():
|
||||
stringified_value = get_stringified_value(child_value)
|
||||
if stringified_value:
|
||||
child[child_key] = stringified_value
|
||||
|
||||
return package
|
||||
|
||||
def get_stringified_value(value):
|
||||
if isinstance(value, datetime.datetime):
|
||||
return frappe.utils.get_datetime_str(value)
|
||||
|
||||
if isinstance(value, datetime.date):
|
||||
return frappe.utils.get_date_str(value)
|
||||
|
||||
if isinstance(value, datetime.timedelta):
|
||||
return frappe.utils.get_time_str(value)
|
||||
|
||||
return None
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestPackagePublishTool(unittest.TestCase):
|
||||
pass
|
||||
|
|
@ -124,6 +124,8 @@ class Database(object):
|
|||
# in transaction validations
|
||||
self.check_transaction_status(query)
|
||||
|
||||
self.clear_db_table_cache(query)
|
||||
|
||||
# autocommit
|
||||
if auto_commit: self.commit()
|
||||
|
||||
|
|
@ -277,6 +279,11 @@ class Database(object):
|
|||
ret.append(frappe._dict(zip(keys, values)))
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def clear_db_table_cache(query):
|
||||
if query and query.strip().split()[0].lower() in {'drop', 'create'}:
|
||||
frappe.cache().delete_key('db_tables')
|
||||
|
||||
@staticmethod
|
||||
def needs_formatting(result, formatted):
|
||||
"""Returns true if the first row in the result has a Date, Datetime, Long Int."""
|
||||
|
|
@ -769,7 +776,16 @@ class Database(object):
|
|||
return ("tab" + doctype) in self.get_tables()
|
||||
|
||||
def get_tables(self):
|
||||
return [d[0] for d in self.sql("select table_name from information_schema.tables where table_schema not in ('pg_catalog', 'information_schema')")]
|
||||
tables = frappe.cache().get_value('db_tables')
|
||||
if not tables:
|
||||
table_rows = self.sql("""
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
||||
""")
|
||||
tables = {d[0] for d in table_rows}
|
||||
frappe.cache().set_value('db_tables', tables)
|
||||
return tables
|
||||
|
||||
def a_row_exists(self, doctype):
|
||||
"""Returns True if atleast one row exists."""
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@ class MariaDBDatabase(Database):
|
|||
'Signature': ('longtext', ''),
|
||||
'Color': ('varchar', self.VARCHAR_LEN),
|
||||
'Barcode': ('longtext', ''),
|
||||
'Geolocation': ('longtext', '')
|
||||
'Geolocation': ('longtext', ''),
|
||||
'Duration': ('decimal', '18,6')
|
||||
}
|
||||
|
||||
def get_connection(self):
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ CREATE TABLE `tabDocField` (
|
|||
`precision` varchar(255) DEFAULT NULL,
|
||||
`length` int(11) NOT NULL DEFAULT 0,
|
||||
`translatable` int(1) NOT NULL DEFAULT 0,
|
||||
`hide_border` int(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`name`),
|
||||
KEY `parent` (`parent`),
|
||||
KEY `label` (`label`),
|
||||
|
|
|
|||
|
|
@ -60,7 +60,8 @@ class PostgresDatabase(Database):
|
|||
'Signature': ('text', ''),
|
||||
'Color': ('varchar', self.VARCHAR_LEN),
|
||||
'Barcode': ('text', ''),
|
||||
'Geolocation': ('text', '')
|
||||
'Geolocation': ('text', ''),
|
||||
'Duration': ('decimal', '18,6')
|
||||
}
|
||||
|
||||
def get_connection(self):
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ CREATE TABLE "tabDocField" (
|
|||
"precision" varchar(255) DEFAULT NULL,
|
||||
"length" bigint NOT NULL DEFAULT 0,
|
||||
"translatable" smallint NOT NULL DEFAULT 0,
|
||||
"hide_border" smallint NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY ("name")
|
||||
) ;
|
||||
|
||||
|
|
|
|||
|
|
@ -137,16 +137,14 @@ class DBTable:
|
|||
if frappe.db.is_missing_column(e):
|
||||
# Unknown column 'column_name' in 'field list'
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
raise
|
||||
|
||||
if max_length and max_length[0][0] and max_length[0][0] > new_length:
|
||||
if col.fieldname in self.columns:
|
||||
self.columns[col.fieldname].length = current_length
|
||||
|
||||
frappe.msgprint(_("""Reverting length to {0} for '{1}' in '{2}';
|
||||
Setting the length as {3} will cause truncation of data.""")
|
||||
.format(current_length, col.fieldname, self.doctype, new_length))
|
||||
info_message = _("Reverting length to {0} for '{1}' in '{2}'. Setting the length as {3} will cause truncation of data.") \
|
||||
.format(current_length, col.fieldname, self.doctype, new_length)
|
||||
frappe.msgprint(info_message)
|
||||
|
||||
def is_new(self):
|
||||
return self.table_name not in frappe.db.get_tables()
|
||||
|
|
|
|||
|
|
@ -21,24 +21,49 @@ class Workspace:
|
|||
self.extended_charts = []
|
||||
self.extended_shortcuts = []
|
||||
|
||||
user = frappe.get_user()
|
||||
user.build_permissions()
|
||||
|
||||
user_doc = frappe.get_doc('User', frappe.session.user)
|
||||
self.blocked_modules = user_doc.get_blocked_modules()
|
||||
self.user = frappe.get_user()
|
||||
self.allowed_modules = self.get_cached_value('user_allowed_modules', self.get_allowed_modules)
|
||||
self.doc = self.get_page_for_user()
|
||||
|
||||
if self.doc.module in self.blocked_modules:
|
||||
if self.doc.module not in self.allowed_modules:
|
||||
raise frappe.PermissionError
|
||||
|
||||
self.user = user
|
||||
self.allowed_pages = get_allowed_pages()
|
||||
self.allowed_reports = get_allowed_reports()
|
||||
self.can_read = self.get_cached_value('user_perm_can_read', self.get_can_read_items)
|
||||
|
||||
self.allowed_pages = get_allowed_pages(cache=True)
|
||||
self.allowed_reports = get_allowed_reports(cache=True)
|
||||
self.onboarding_doc = self.get_onboarding_doc()
|
||||
self.onboarding = None
|
||||
|
||||
self.table_counts = get_table_with_counts()
|
||||
self.restricted_doctypes = frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache()
|
||||
self.restricted_pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache()
|
||||
|
||||
def get_cached_value(self, cache_key, fallback_fn):
|
||||
_cache = frappe.cache()
|
||||
|
||||
value = _cache.get_value(cache_key, user=frappe.session.user)
|
||||
if value:
|
||||
return value
|
||||
|
||||
value = fallback_fn()
|
||||
|
||||
# Expire every six hour
|
||||
_cache.set_value(cache_key, value, frappe.session.user, 21600)
|
||||
return value
|
||||
|
||||
def get_can_read_items(self):
|
||||
if not self.user.can_read:
|
||||
self.user.build_permissions()
|
||||
|
||||
return self.user.can_read
|
||||
|
||||
def get_allowed_modules(self):
|
||||
if not self.user.allow_modules:
|
||||
self.user.build_permissions()
|
||||
|
||||
return self.user.allow_modules
|
||||
|
||||
def get_page_for_user(self):
|
||||
filters = {
|
||||
'extends': self.page_name,
|
||||
|
|
@ -51,12 +76,37 @@ class Workspace:
|
|||
self.get_pages_to_extend()
|
||||
return frappe.get_doc("Desk Page", self.page_name)
|
||||
|
||||
def get_onboarding_doc(self):
|
||||
# Check if onboarding is enabled
|
||||
if not frappe.get_system_settings("enable_onboarding"):
|
||||
return None
|
||||
|
||||
if not self.doc.onboarding:
|
||||
return None
|
||||
|
||||
if frappe.db.get_value("Module Onboarding", self.doc.onboarding, "is_complete"):
|
||||
return None
|
||||
|
||||
doc = frappe.get_doc("Module Onboarding", self.doc.onboarding)
|
||||
|
||||
# Check if user is allowed
|
||||
allowed_roles = set(doc.get_allowed_roles())
|
||||
user_roles = set(frappe.get_roles())
|
||||
if not allowed_roles & user_roles:
|
||||
return None
|
||||
|
||||
# Check if already complete
|
||||
if doc.check_completion():
|
||||
return None
|
||||
|
||||
return doc
|
||||
|
||||
def get_pages_to_extend(self):
|
||||
pages = frappe.get_all("Desk Page", filters={
|
||||
"extends": self.page_name,
|
||||
'restrict_to_domain': ['in', frappe.get_active_domains()],
|
||||
'for_user': '',
|
||||
'module': ['not in', self.blocked_modules]
|
||||
'module': ['in', self.allowed_modules]
|
||||
})
|
||||
|
||||
pages = [frappe.get_doc("Desk Page", page['name']) for page in pages]
|
||||
|
|
@ -70,13 +120,15 @@ class Workspace:
|
|||
item_type = item_type.lower()
|
||||
|
||||
if item_type == "doctype":
|
||||
return (name in self.user.can_read and name in self.restricted_doctypes)
|
||||
return (name in self.can_read and name in self.restricted_doctypes)
|
||||
if item_type == "page":
|
||||
return (name in self.allowed_pages and name in self.restricted_pages)
|
||||
if item_type == "report":
|
||||
return name in self.allowed_reports
|
||||
if item_type == "help":
|
||||
return True
|
||||
if item_type == "dashboard":
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
|
@ -96,16 +148,29 @@ class Workspace:
|
|||
'items': self.get_shortcuts()
|
||||
}
|
||||
|
||||
if self.onboarding_doc:
|
||||
self.onboarding = {
|
||||
'label': _(self.onboarding_doc.title),
|
||||
'subtitle': _(self.onboarding_doc.subtitle),
|
||||
'success': _(self.onboarding_doc.success_message),
|
||||
'docs_url': self.onboarding_doc.documentation_url,
|
||||
'user_can_dismiss': self.onboarding_doc.user_can_dismiss,
|
||||
'items': self.get_onboarding_steps()
|
||||
}
|
||||
|
||||
def get_cards(self):
|
||||
cards = self.doc.cards + get_custom_reports_and_doctypes(self.doc.module)
|
||||
cards = self.doc.cards
|
||||
if not self.doc.hide_custom:
|
||||
cards = cards + get_custom_reports_and_doctypes(self.doc.module)
|
||||
|
||||
if len(self.extended_cards):
|
||||
cards = cards + self.extended_cards
|
||||
default_country = frappe.db.get_default("country")
|
||||
|
||||
def _doctype_contains_a_record(name):
|
||||
exists = self.table_counts.get(name)
|
||||
if not exists:
|
||||
if not frappe.db.get_value('DocType', name, 'issingle'):
|
||||
exists = self.table_counts.get(name, None)
|
||||
if exists is None:
|
||||
if not frappe.db.get_value('DocType', name, 'issingle', cache=True):
|
||||
exists = frappe.db.count(name)
|
||||
else:
|
||||
exists = True
|
||||
|
|
@ -207,6 +272,18 @@ class Workspace:
|
|||
|
||||
return items
|
||||
|
||||
def get_onboarding_steps(self):
|
||||
steps = []
|
||||
for doc in self.onboarding_doc.get_steps():
|
||||
step = doc.as_dict().copy()
|
||||
step.label = _(doc.title)
|
||||
if step.action == "Create Entry":
|
||||
step.is_submittable = frappe.db.get_value("DocType", step.reference_document, 'is_submittable', cache=True)
|
||||
steps.append(step)
|
||||
|
||||
return steps
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.read_only()
|
||||
def get_desktop_page(page):
|
||||
|
|
@ -226,6 +303,7 @@ def get_desktop_page(page):
|
|||
'charts': wspace.charts,
|
||||
'shortcuts': wspace.shortcuts,
|
||||
'cards': wspace.cards,
|
||||
'onboarding': wspace.onboarding,
|
||||
'allow_customization': not wspace.doc.disable_user_customization
|
||||
}
|
||||
|
||||
|
|
@ -244,7 +322,6 @@ def get_desk_sidebar_items(flatten=False):
|
|||
filters = {
|
||||
'restrict_to_domain': ['in', frappe.get_active_domains()],
|
||||
'extends_another_page': 0,
|
||||
'is_standard': 1,
|
||||
'for_user': '',
|
||||
'module': ['not in', blocked_modules]
|
||||
}
|
||||
|
|
@ -360,8 +437,8 @@ def save_customization(page, config):
|
|||
"charts_label": original_page.charts_label,
|
||||
"cards_label": original_page.cards_label,
|
||||
"shortcuts_label": original_page.shortcuts_label,
|
||||
"icon": original_page.icon,
|
||||
"module": original_page.module,
|
||||
"onboarding": original_page.onboarding,
|
||||
"developer_mode_only": original_page.developer_mode_only,
|
||||
"category": original_page.category
|
||||
})
|
||||
|
|
@ -432,3 +509,16 @@ 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
|
||||
|
||||
"""
|
||||
frappe.db.set_value("Onboarding Step", name, field, value)
|
||||
|
|
|
|||
|
|
@ -4,5 +4,21 @@
|
|||
frappe.ui.form.on('Dashboard', {
|
||||
refresh: function(frm) {
|
||||
frm.add_custom_button(__("Show Dashboard"), () => frappe.set_route('dashboard', frm.doc.name));
|
||||
|
||||
frm.set_query("chart", "charts", function() {
|
||||
return {
|
||||
filters: {
|
||||
is_public: 1
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("card", "cards", function() {
|
||||
return {
|
||||
filters: {
|
||||
is_public: 1
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@
|
|||
"field_order": [
|
||||
"dashboard_name",
|
||||
"is_default",
|
||||
"charts"
|
||||
"charts",
|
||||
"chart_options",
|
||||
"cards"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -31,10 +33,23 @@
|
|||
"label": "Charts",
|
||||
"options": "Dashboard Chart Link",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"description": "Set Default Options for all charts on this Dashboard (Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"])",
|
||||
"fieldname": "chart_options",
|
||||
"fieldtype": "Code",
|
||||
"label": "Chart Options",
|
||||
"options": "JSON"
|
||||
},
|
||||
{
|
||||
"fieldname": "cards",
|
||||
"fieldtype": "Table",
|
||||
"label": "Cards",
|
||||
"options": "Number Card Link"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-03-25 21:09:37.080132",
|
||||
"modified": "2020-04-29 13:26:37.362482",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Dashboard",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
from __future__ import unicode_literals
|
||||
from frappe.model.document import Document
|
||||
import frappe
|
||||
from frappe import _
|
||||
import json
|
||||
|
||||
class Dashboard(Document):
|
||||
def on_update(self):
|
||||
|
|
@ -13,11 +15,36 @@ class Dashboard(Document):
|
|||
frappe.db.sql('''update
|
||||
tabDashboard set is_default = 0 where name != %s''', self.name)
|
||||
|
||||
def validate(self):
|
||||
self.validate_custom_options()
|
||||
|
||||
def validate_custom_options(self):
|
||||
if self.chart_options:
|
||||
try:
|
||||
json.loads(self.chart_options)
|
||||
except ValueError as error:
|
||||
frappe.throw(_("Invalid json added in the custom options: {0}").format(error))
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_permitted_charts(dashboard_name):
|
||||
permitted_charts = []
|
||||
dashboard = frappe.get_doc('Dashboard', dashboard_name)
|
||||
for chart in dashboard.charts:
|
||||
if frappe.has_permission('Dashboard Chart', doc=chart.chart):
|
||||
permitted_charts.append(chart)
|
||||
chart_dict = frappe._dict()
|
||||
chart_dict.update(chart.as_dict())
|
||||
|
||||
if dashboard.get('chart_options'):
|
||||
chart_dict.custom_options = dashboard.get('chart_options')
|
||||
permitted_charts.append(chart_dict)
|
||||
|
||||
return permitted_charts
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_permitted_cards(dashboard_name):
|
||||
permitted_cards = []
|
||||
dashboard = frappe.get_doc('Dashboard', dashboard_name)
|
||||
for card in dashboard.cards:
|
||||
if frappe.has_permission('Number Card', doc=card.card):
|
||||
permitted_cards.append(card)
|
||||
return permitted_cards
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
frm.add_fetch('source', 'timeseries', 'timeseries');
|
||||
},
|
||||
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.chart_filters = null;
|
||||
frm.add_custom_button('Add Chart to Dashboard', () => {
|
||||
|
|
@ -48,6 +49,7 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
});
|
||||
|
||||
frm.set_df_property("filters_section", "hidden", 1);
|
||||
frm.trigger('set_time_series');
|
||||
frm.set_query('document_type', function() {
|
||||
return {
|
||||
filters: {
|
||||
|
|
@ -56,16 +58,31 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
}
|
||||
});
|
||||
frm.trigger('update_options');
|
||||
frm.trigger('set_heatmap_year_options');
|
||||
if (frm.doc.report_name) {
|
||||
frm.trigger('set_chart_report_filters');
|
||||
}
|
||||
|
||||
if (!frappe.boot.developer_mode) {
|
||||
frm.set_df_property("custom_options", "hidden", 1);
|
||||
}
|
||||
},
|
||||
|
||||
source: function(frm) {
|
||||
frm.trigger("show_filters");
|
||||
},
|
||||
|
||||
set_heatmap_year_options: function(frm) {
|
||||
if (frm.doc.type == 'Heatmap') {
|
||||
frappe.db.get_doc('System Settings').then(doc => {
|
||||
const creation_date = doc.creation;
|
||||
frm.set_df_property('heatmap_year', 'options', frappe.dashboard_utils.get_years_since_creation(creation_date));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
chart_type: function(frm) {
|
||||
frm.trigger('set_time_series');
|
||||
if (frm.doc.chart_type == 'Report') {
|
||||
frm.set_query('report_name', () => {
|
||||
return {
|
||||
|
|
@ -75,23 +92,19 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
}
|
||||
});
|
||||
} else {
|
||||
// set timeseries based on chart type
|
||||
if (['Count', 'Average', 'Sum'].includes(frm.doc.chart_type)) {
|
||||
frm.set_value('timeseries', 1);
|
||||
} else {
|
||||
frm.set_value('timeseries', 0);
|
||||
}
|
||||
|
||||
if (frm.doc.chart_type == 'Group By') {
|
||||
frm.set_df_property('type', 'options', ['Line', 'Bar', 'Percentage', 'Pie']);
|
||||
} else {
|
||||
frm.set_df_property('type', 'options', ['Line', 'Bar']);
|
||||
}
|
||||
|
||||
frm.set_value('document_type', '');
|
||||
}
|
||||
},
|
||||
|
||||
set_time_series: function(frm) {
|
||||
// set timeseries based on chart type
|
||||
if (['Count', 'Average', 'Sum'].includes(frm.doc.chart_type)) {
|
||||
frm.set_value('timeseries', 1);
|
||||
} else {
|
||||
frm.set_value('timeseries', 0);
|
||||
}
|
||||
},
|
||||
|
||||
document_type: function(frm) {
|
||||
// update `based_on` options based on date / datetime fields
|
||||
frm.set_value('source', '');
|
||||
|
|
@ -238,6 +251,7 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
render_filters_table: function(frm) {
|
||||
frm.set_df_property("filters_section", "hidden", 0);
|
||||
let is_document_type = frm.doc.chart_type!== 'Report' && frm.doc.chart_type!=='Custom';
|
||||
let is_dynamic_filter = f => ['Date', 'DateRange'].includes(f.fieldtype) && f.default;
|
||||
|
||||
let wrapper = $(frm.get_field('filters_json').wrapper).empty();
|
||||
let table = $(`<table class="table table-bordered" style="cursor:pointer; margin:0px;">
|
||||
|
|
@ -255,6 +269,18 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
let filters = JSON.parse(frm.doc.filters_json || '[]');
|
||||
var filters_set = false;
|
||||
|
||||
// Set dynamic filters for reports
|
||||
if (frm.doc.chart_type == 'Report') {
|
||||
let set_filters = false;
|
||||
frm.chart_filters.forEach(f => {
|
||||
if (is_dynamic_filter(f)) {
|
||||
filters[f.fieldname] = f.default;
|
||||
set_filters = true;
|
||||
}
|
||||
});
|
||||
set_filters && frm.set_value('filters_json', JSON.stringify(filters));
|
||||
}
|
||||
|
||||
let fields;
|
||||
if (is_document_type) {
|
||||
fields = [
|
||||
|
|
@ -278,16 +304,7 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
});
|
||||
}
|
||||
} else if (frm.chart_filters.length) {
|
||||
fields = frm.chart_filters.filter(f => {
|
||||
if (f.on_change && !f.reqd) {
|
||||
return false;
|
||||
}
|
||||
if (f.get_query || f.get_data) {
|
||||
f.read_only = 1;
|
||||
}
|
||||
|
||||
return f.fieldname;
|
||||
});
|
||||
fields = frm.chart_filters.filter(f => f.fieldname);
|
||||
|
||||
fields.map( f => {
|
||||
if (filters[f.fieldname]) {
|
||||
|
|
@ -315,7 +332,7 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __('Set Filters'),
|
||||
fields: fields,
|
||||
fields: fields.filter(f => !is_dynamic_filter(f)),
|
||||
primary_action: function() {
|
||||
let values = this.get_values();
|
||||
if (values) {
|
||||
|
|
@ -348,10 +365,17 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
}
|
||||
|
||||
dialog.show();
|
||||
|
||||
if (frm.doc.chart_type == 'Report') {
|
||||
//Set query report object so that it can be used while fetching filter values in the report
|
||||
frappe.query_report = new frappe.views.QueryReport({'filters': dialog.fields_list});
|
||||
frappe.query_reports[frm.doc.report_name]
|
||||
&& frappe.query_reports[frm.doc.report_name].onload
|
||||
&& frappe.query_reports[frm.doc.report_name].onload(frappe.query_report);
|
||||
}
|
||||
|
||||
dialog.set_values(filters);
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -22,17 +22,20 @@
|
|||
"aggregate_function_based_on",
|
||||
"number_of_groups",
|
||||
"column_break_6",
|
||||
"is_public",
|
||||
"heatmap_year",
|
||||
"timespan",
|
||||
"from_date",
|
||||
"to_date",
|
||||
"time_interval",
|
||||
"timeseries",
|
||||
"type",
|
||||
"filters_section",
|
||||
"filters_json",
|
||||
"chart_options_section",
|
||||
"type",
|
||||
"column_break_2",
|
||||
"color",
|
||||
"column_break_2",
|
||||
"custom_options",
|
||||
"section_break_10",
|
||||
"last_synced_on"
|
||||
],
|
||||
|
|
@ -83,14 +86,14 @@
|
|||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "timeseries",
|
||||
"depends_on": "eval: doc.timeseries && doc.type !== 'Heatmap'",
|
||||
"fieldname": "timespan",
|
||||
"fieldtype": "Select",
|
||||
"label": "Timespan",
|
||||
"options": "Last Year\nLast Quarter\nLast Month\nLast Week\nSelect Date Range"
|
||||
},
|
||||
{
|
||||
"depends_on": "timeseries",
|
||||
"depends_on": "eval: doc.timeseries && doc.type !== 'Heatmap'",
|
||||
"fieldname": "time_interval",
|
||||
"fieldtype": "Select",
|
||||
"label": "Time Interval",
|
||||
|
|
@ -98,7 +101,7 @@
|
|||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.chart_type !== 'Group By'",
|
||||
"depends_on": "eval: !['Group By', 'Report'].includes(doc.chart_type)\n",
|
||||
"fieldname": "timeseries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Time Series"
|
||||
|
|
@ -121,18 +124,18 @@
|
|||
"label": "Chart Options"
|
||||
},
|
||||
{
|
||||
"default": "Line",
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Type",
|
||||
"options": "Line\nBar\nPercentage\nPie",
|
||||
"reqd": 1
|
||||
"options": "Line\nBar\nPercentage\nPie\nDonut\nHeatmap"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.chart_type !== 'Report'",
|
||||
"depends_on": "eval: doc.chart_type !== 'Report' && doc.type !== 'Heatmap'",
|
||||
"fieldname": "color",
|
||||
"fieldtype": "Color",
|
||||
"label": "Color"
|
||||
|
|
@ -213,10 +216,29 @@
|
|||
"label": "Y Axis",
|
||||
"mandatory_depends_on": "eval:doc.report_name && !doc.is_custom",
|
||||
"options": "Dashboard Chart Field"
|
||||
},
|
||||
{
|
||||
"description": "Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"]",
|
||||
"fieldname": "custom_options",
|
||||
"fieldtype": "Code",
|
||||
"label": "Custom Options"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "This chart will be available to all Users if this is set",
|
||||
"fieldname": "is_public",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Public"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.type == 'Heatmap'",
|
||||
"fieldname": "heatmap_year",
|
||||
"fieldtype": "Select",
|
||||
"label": "Year"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-04-08 18:54:36.739183",
|
||||
"modified": "2020-05-16 15:03:02.455395",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Dashboard Chart",
|
||||
|
|
@ -247,6 +269,7 @@
|
|||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import frappe
|
|||
from frappe import _
|
||||
import datetime
|
||||
import json
|
||||
from frappe.core.page.dashboard.dashboard import cache_source, get_from_date_from_timespan
|
||||
from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate, get_datetime
|
||||
from frappe.utils.dashboard import cache_source, get_from_date_from_timespan
|
||||
from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate, get_datetime, cint
|
||||
from frappe.model.naming import append_number_if_name_exists
|
||||
from frappe.boot import get_allowed_reports
|
||||
from frappe.model.document import Document
|
||||
|
|
@ -27,7 +27,7 @@ def get_permission_query_conditions(user):
|
|||
return None
|
||||
|
||||
allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read())
|
||||
allowed_reports = tuple([key.encode('UTF8') for key in get_allowed_reports()])
|
||||
allowed_reports = tuple([key if type(key) == str else key.encode('UTF8') for key in get_allowed_reports()])
|
||||
|
||||
return '''
|
||||
`tabDashboard Chart`.`document_type` in {allowed_doctypes}
|
||||
|
|
@ -58,13 +58,13 @@ def has_permission(doc, ptype, user):
|
|||
@frappe.whitelist()
|
||||
@cache_source
|
||||
def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None,
|
||||
to_date = None, timespan = None, time_interval = None, refresh = None):
|
||||
to_date = None, timespan = None, time_interval = None, heatmap_year=None, refresh = None):
|
||||
if chart_name:
|
||||
chart = frappe.get_doc('Dashboard Chart', chart_name)
|
||||
else:
|
||||
chart = frappe._dict(frappe.parse_json(chart))
|
||||
|
||||
|
||||
heatmap_year = heatmap_year or chart.heatmap_year
|
||||
timespan = timespan or chart.timespan
|
||||
|
||||
if timespan == 'Select Date Range':
|
||||
|
|
@ -76,10 +76,10 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
|
|||
if to_date and len(to_date):
|
||||
to_date = get_datetime(to_date)
|
||||
else:
|
||||
to_date = chart.to_date
|
||||
to_date = get_datetime(chart.to_date)
|
||||
|
||||
timegrain = time_interval or chart.time_interval
|
||||
filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json)
|
||||
filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json) or []
|
||||
|
||||
# don't include cancelled documents
|
||||
filters.append([chart.document_type, 'docstatus', '<', 2, False])
|
||||
|
|
@ -87,27 +87,41 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
|
|||
if chart.chart_type == 'Group By':
|
||||
chart_config = get_group_by_chart_config(chart, filters)
|
||||
else:
|
||||
chart_config = get_chart_config(chart, filters, timespan, timegrain, from_date, to_date)
|
||||
if chart.type == 'Heatmap':
|
||||
chart_config = get_heatmap_chart_config(chart, filters, heatmap_year)
|
||||
else:
|
||||
chart_config = get_chart_config(chart, filters, timespan, timegrain, from_date, to_date)
|
||||
|
||||
return chart_config
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_report_chart(args):
|
||||
def create_dashboard_chart(args):
|
||||
args = frappe.parse_json(args)
|
||||
_doc = frappe.new_doc('Dashboard Chart')
|
||||
doc = frappe.new_doc('Dashboard Chart')
|
||||
|
||||
doc.update(args)
|
||||
|
||||
if args.get('custom_options'):
|
||||
doc.custom_options = json.dumps(args.get('custom_options'))
|
||||
|
||||
_doc.update(args)
|
||||
if frappe.db.exists('Dashboard Chart', args.chart_name):
|
||||
args.chart_name = append_number_if_name_exists('Dashboard Chart', args.chart_name)
|
||||
_doc.chart_name = args.chart_name
|
||||
_doc.insert(ignore_permissions=True)
|
||||
doc.chart_name = args.chart_name
|
||||
doc.insert(ignore_permissions=True)
|
||||
return doc
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_report_chart(args):
|
||||
doc = create_dashboard_chart(args)
|
||||
args = frappe.parse_json(args)
|
||||
args.chart_name = doc.chart_name
|
||||
if args.dashboard:
|
||||
add_chart_to_dashboard(json.dumps(args))
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_chart_to_dashboard(args):
|
||||
args = frappe.parse_json(args)
|
||||
|
||||
dashboard = frappe.get_doc('Dashboard', args.dashboard)
|
||||
dashboard_link = frappe.new_doc('Dashboard Chart Link')
|
||||
dashboard_link.chart = args.chart_name
|
||||
|
|
@ -123,7 +137,6 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
|
|||
to_date = datetime.datetime.now()
|
||||
|
||||
doctype = chart.document_type
|
||||
unit_function = get_unit_function(doctype, chart.based_on, timegrain)
|
||||
datefield = chart.based_on
|
||||
aggregate_function = get_aggregate_function(chart.chart_type)
|
||||
value_field = chart.value_based_on or '1'
|
||||
|
|
@ -133,26 +146,21 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
|
|||
filters.append([doctype, datefield, '>=', from_date, False])
|
||||
filters.append([doctype, datefield, '<=', to_date, False])
|
||||
|
||||
data = frappe.db.get_all(
|
||||
data = frappe.db.get_list(
|
||||
doctype,
|
||||
fields = [
|
||||
'extract(year from `tab{doctype}`.{datefield}) as _year'.format(doctype=doctype, datefield=datefield),
|
||||
'{} as _unit'.format(unit_function),
|
||||
'{} as _unit'.format(datefield),
|
||||
'{aggregate_function}({value_field})'.format(aggregate_function=aggregate_function, value_field=value_field),
|
||||
],
|
||||
filters = filters,
|
||||
group_by = '_year, _unit',
|
||||
order_by = '_year asc, _unit asc',
|
||||
group_by = '_unit',
|
||||
order_by = '_unit asc',
|
||||
as_list = True,
|
||||
ignore_ifnull = True
|
||||
)
|
||||
|
||||
result = get_result(data, timegrain, from_date, to_date)
|
||||
|
||||
# result given as year, unit -> convert it to end of period of that unit
|
||||
result = convert_to_dates(data, timegrain)
|
||||
|
||||
# add missing data points for periods where there was no result
|
||||
result = add_missing_values(result, timegrain, timespan, from_date, to_date)
|
||||
chart_config = {
|
||||
"labels": [formatdate(r[0].strftime('%Y-%m-%d')) for r in result],
|
||||
"datasets": [{
|
||||
|
|
@ -163,6 +171,41 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
|
|||
|
||||
return chart_config
|
||||
|
||||
def get_heatmap_chart_config(chart, filters, heatmap_year):
|
||||
aggregate_function = get_aggregate_function(chart.chart_type)
|
||||
value_field = chart.value_based_on or '1'
|
||||
doctype = chart.document_type
|
||||
datefield = chart.based_on
|
||||
year = cint(heatmap_year) if heatmap_year else getdate(nowdate()).year
|
||||
year_start_date = datetime.date(year, 1, 1).strftime('%Y-%m-%d')
|
||||
next_year_start_date = datetime.date(year + 1, 1, 1).strftime('%Y-%m-%d')
|
||||
|
||||
filters.append([doctype, datefield, '>', "{date}".format(date=year_start_date), False])
|
||||
filters.append([doctype, datefield, '<', "{date}".format(date=next_year_start_date), False])
|
||||
|
||||
if frappe.db.db_type == 'mariadb':
|
||||
timestamp_field = 'unix_timestamp({datefield})'.format(datefield=datefield)
|
||||
else:
|
||||
timestamp_field = 'extract(epoch from timestamp {datefield})'.format(datefield=datefield)
|
||||
|
||||
data = dict(frappe.db.get_all(
|
||||
doctype,
|
||||
fields = [
|
||||
timestamp_field,
|
||||
'{aggregate_function}({value_field})'.format(aggregate_function=aggregate_function, value_field=value_field),
|
||||
],
|
||||
filters = filters,
|
||||
group_by = 'date({datefield})'.format(datefield=datefield),
|
||||
as_list = 1,
|
||||
order_by = '{datefield} asc'.format(datefield=datefield),
|
||||
ignore_ifnull = True
|
||||
))
|
||||
|
||||
chart_config = {
|
||||
'labels': [],
|
||||
'dataPoints': data,
|
||||
}
|
||||
return chart_config
|
||||
|
||||
def get_group_by_chart_config(chart, filters):
|
||||
|
||||
|
|
@ -171,7 +214,7 @@ def get_group_by_chart_config(chart, filters):
|
|||
group_by_field = chart.group_by_based_on
|
||||
doctype = chart.document_type
|
||||
|
||||
data = frappe.db.get_all(
|
||||
data = frappe.db.get_list(
|
||||
doctype,
|
||||
fields = [
|
||||
'{} as name'.format(group_by_field),
|
||||
|
|
@ -212,75 +255,22 @@ def get_aggregate_function(chart_type):
|
|||
}[chart_type]
|
||||
|
||||
|
||||
def convert_to_dates(data, timegrain):
|
||||
""" Converts individual dates within data to the end of period """
|
||||
result = []
|
||||
for d in data:
|
||||
if d[2] != 0:
|
||||
if timegrain == 'Daily':
|
||||
result.append([add_to_date('{:d}-01-01'.format(int(d[0])), days = d[1] - 1), d[2]])
|
||||
elif timegrain == 'Weekly':
|
||||
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), weeks = d[1] + 1), days = -1), d[2]])
|
||||
elif timegrain == 'Monthly':
|
||||
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=d[1]), days = -1), d[2]])
|
||||
elif timegrain == 'Quarterly':
|
||||
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=d[1] * 3), days = -1), d[2]])
|
||||
elif timegrain == 'Yearly':
|
||||
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=12), days = -1), d[2]])
|
||||
result[-1][0] = getdate(result[-1][0])
|
||||
|
||||
return result
|
||||
|
||||
def get_unit_function(doctype, datefield, timegrain):
|
||||
unit_function = ''
|
||||
if timegrain=='Daily':
|
||||
if frappe.db.db_type == 'mariadb':
|
||||
unit_function = 'dayofyear(`tab{doctype}`.{datefield})'.format(
|
||||
doctype=doctype, datefield=datefield)
|
||||
else:
|
||||
unit_function = 'extract(doy from `tab{doctype}`.{datefield})'.format(
|
||||
doctype=doctype, datefield=datefield)
|
||||
|
||||
else:
|
||||
unit_function = 'extract({unit} from `tab{doctype}`.{datefield})'.format(
|
||||
unit = timegrain[:-2].lower(), doctype=doctype, datefield=datefield)
|
||||
|
||||
return unit_function
|
||||
|
||||
def add_missing_values(data, timegrain, timespan, from_date, to_date):
|
||||
# add missing intervals
|
||||
def get_result(data, timegrain, from_date, to_date):
|
||||
start_date = getdate(from_date)
|
||||
end_date = getdate(to_date)
|
||||
result = []
|
||||
|
||||
if timespan != 'All Time':
|
||||
first_expected_date = get_period_ending(from_date, timegrain)
|
||||
# fill out data before the first data point
|
||||
first_data_point_date = data[0][0] if data else getdate(add_to_date(to_date, days=1))
|
||||
while first_data_point_date > first_expected_date:
|
||||
result.append([first_expected_date, 0.0])
|
||||
first_expected_date = get_next_expected_date(first_expected_date, timegrain)
|
||||
while start_date <= end_date:
|
||||
next_date = get_next_expected_date(start_date, timegrain)
|
||||
result.append([next_date, 0.0])
|
||||
start_date = next_date
|
||||
|
||||
# fill data points and missing points
|
||||
for i, d in enumerate(data):
|
||||
result.append(d)
|
||||
|
||||
next_expected_date = get_next_expected_date(d[0], timegrain)
|
||||
|
||||
if i < len(data)-1:
|
||||
next_date = data[i+1][0]
|
||||
else:
|
||||
# already reached at end of data, see if we need any more dates
|
||||
next_date = getdate(nowdate())
|
||||
|
||||
# if next data point is earler than the expected date
|
||||
# need to fill out missing data points
|
||||
while next_date > next_expected_date:
|
||||
# fill missing value
|
||||
result.append([next_expected_date, 0.0])
|
||||
next_expected_date = get_next_expected_date(next_expected_date, timegrain)
|
||||
|
||||
# add date for the last period (if missing)
|
||||
if result and get_period_ending(to_date, timegrain) > result[-1][0]:
|
||||
result.append([get_period_ending(to_date, timegrain), 0.0])
|
||||
data_index = 0
|
||||
if data:
|
||||
for i, d in enumerate(result):
|
||||
while data_index < len(data) and getdate(data[data_index][0]) <= d[0]:
|
||||
d[1] += data[data_index][1]
|
||||
data_index += 1
|
||||
|
||||
return result
|
||||
|
||||
|
|
@ -309,17 +299,12 @@ def get_period_ending(date, timegrain):
|
|||
return getdate(date)
|
||||
|
||||
def get_week_ending(date):
|
||||
# fun fact: week ends on the day before 1st Jan of the year.
|
||||
# for 2019 it is Monday
|
||||
# week starts on monday
|
||||
from datetime import timedelta
|
||||
start = date - timedelta(days = date.weekday())
|
||||
end = start + timedelta(days=6)
|
||||
|
||||
week_of_the_year = int(date.strftime('%U'))
|
||||
|
||||
if week_of_the_year == 52:
|
||||
date = add_to_date(date, years=1)
|
||||
# first day of next week
|
||||
date = add_to_date('{}-01-01'.format(date.year), weeks = (week_of_the_year%52) + 1)
|
||||
# last day of this week
|
||||
return add_to_date(date, days=-1)
|
||||
return end
|
||||
|
||||
def get_month_ending(date):
|
||||
month_of_the_year = int(date.strftime('%m'))
|
||||
|
|
@ -351,6 +336,13 @@ def get_year_ending(date):
|
|||
# last day of this month
|
||||
return add_to_date(date, days=-1)
|
||||
|
||||
def get_charts_for_user(doctype, txt, searchfield, start, page_len, filters):
|
||||
or_filters = {'owner': frappe.session.user, 'is_public': 1}
|
||||
return frappe.db.get_list('Dashboard Chart',
|
||||
fields=['name'],
|
||||
filters=filters,
|
||||
or_filters=or_filters,
|
||||
as_list = 1)
|
||||
|
||||
class DashboardChart(Document):
|
||||
|
||||
|
|
@ -362,6 +354,8 @@ class DashboardChart(Document):
|
|||
self.check_required_field()
|
||||
self.check_document_type()
|
||||
|
||||
self.validate_custom_options()
|
||||
|
||||
def check_required_field(self):
|
||||
if not self.document_type:
|
||||
frappe.throw(_("Document type is required to create a dashboard chart"))
|
||||
|
|
@ -377,4 +371,11 @@ class DashboardChart(Document):
|
|||
|
||||
def check_document_type(self):
|
||||
if frappe.get_meta(self.document_type).issingle:
|
||||
frappe.throw("You cannot create a dashboard chart from single DocTypes")
|
||||
frappe.throw(_("You cannot create a dashboard chart from single DocTypes"))
|
||||
|
||||
def validate_custom_options(self):
|
||||
if self.custom_options:
|
||||
try:
|
||||
json.loads(self.custom_options)
|
||||
except ValueError as error:
|
||||
frappe.throw(_("Invalid json added in the custom options: {0}").format(error))
|
||||
|
|
|
|||
|
|
@ -17,10 +17,9 @@ class TestDashboardChart(unittest.TestCase):
|
|||
self.assertEqual(get_period_ending('2019-04-10', 'Daily'),
|
||||
getdate('2019-04-10'))
|
||||
|
||||
# fun fact: week ends on the day before 1st Jan of the year.
|
||||
# for 2019 it is Monday
|
||||
# week starts on monday
|
||||
self.assertEqual(get_period_ending('2019-04-10', 'Weekly'),
|
||||
getdate('2019-04-15'))
|
||||
getdate('2019-04-14'))
|
||||
|
||||
self.assertEqual(get_period_ending('2019-04-10', 'Monthly'),
|
||||
getdate('2019-04-30'))
|
||||
|
|
@ -133,6 +132,34 @@ class TestDashboardChart(unittest.TestCase):
|
|||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_weekly_dashboard_chart(self):
|
||||
insert_test_records()
|
||||
|
||||
if frappe.db.exists('Dashboard Chart', 'Test Weekly Dashboard Chart'):
|
||||
frappe.delete_doc('Dashboard Chart', 'Test Weekly Dashboard Chart')
|
||||
|
||||
frappe.get_doc(dict(
|
||||
doctype = 'Dashboard Chart',
|
||||
chart_name = 'Test Weekly Dashboard Chart',
|
||||
chart_type = 'Sum',
|
||||
document_type = 'Communication',
|
||||
based_on = 'communication_date',
|
||||
value_based_on = 'rating',
|
||||
timespan = 'Select Date Range',
|
||||
time_interval = 'Weekly',
|
||||
from_date = datetime(2018, 12, 30),
|
||||
to_date = datetime(2019, 1, 15),
|
||||
filters_json = '[]',
|
||||
timeseries = 1
|
||||
)).insert()
|
||||
|
||||
result = get(chart_name ='Test Weekly Dashboard Chart', refresh = 1)
|
||||
|
||||
self.assertEqual(result.get('datasets')[0].get('values'), [200.0, 400.0, 0.0])
|
||||
self.assertEqual(result.get('labels'), [formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')])
|
||||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_group_by_chart_type(self):
|
||||
if frappe.db.exists('Dashboard Chart', 'Test Group By Dashboard Chart'):
|
||||
frappe.delete_doc('Dashboard Chart', 'Test Group By Dashboard Chart')
|
||||
|
|
@ -155,17 +182,16 @@ class TestDashboardChart(unittest.TestCase):
|
|||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_dashboard_with_single_doctype(self):
|
||||
if frappe.db.exists('Dashboard Chart', 'Test Single DocType In Dashboard Chart'):
|
||||
frappe.delete_doc('Dashboard Chart', 'Test Single DocType In Dashboard Chart')
|
||||
def insert_test_records():
|
||||
create_new_communication(datetime(2019, 1, 10), 100)
|
||||
create_new_communication(datetime(2019, 1, 6), 200)
|
||||
create_new_communication(datetime(2019, 1, 8), 300)
|
||||
|
||||
chart_doc = frappe.get_doc(dict(
|
||||
doctype = 'Dashboard Chart',
|
||||
chart_name = 'Test Single DocType In Dashboard Chart',
|
||||
chart_type = 'Count',
|
||||
document_type = 'System Settings',
|
||||
group_by_based_on = 'Created On',
|
||||
filters_json = '{}',
|
||||
))
|
||||
|
||||
self.assertRaises(frappe.ValidationError, chart_doc.insert)
|
||||
def create_new_communication(date, rating):
|
||||
communication = {
|
||||
'doctype': 'Communication',
|
||||
'subject': 'Test Communication',
|
||||
'rating': rating,
|
||||
'communication_date': date
|
||||
}
|
||||
frappe.get_doc(communication).insert()
|
||||
|
|
|
|||
|
|
@ -2,16 +2,22 @@
|
|||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Desk Page', {
|
||||
setup: function(frm) {
|
||||
refresh: function(frm) {
|
||||
frm.enable_save();
|
||||
frm.get_field("is_standard").toggle(frappe.boot.developer_mode);
|
||||
frm.get_field("extends_another_page").toggle(frappe.boot.developer_mode);
|
||||
if (!frappe.boot.developer_mode || frm.doc.for_user) {
|
||||
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)) {
|
||||
frm.trigger('disable_form');
|
||||
}
|
||||
},
|
||||
|
||||
disable_form: function(frm) {
|
||||
frm.set_read_only();
|
||||
frm.fields
|
||||
.filter(field => field.has_input)
|
||||
.forEach(field => {
|
||||
|
|
|
|||
|
|
@ -8,13 +8,12 @@
|
|||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"label",
|
||||
"extends",
|
||||
"for_user",
|
||||
"extends",
|
||||
"module",
|
||||
"category",
|
||||
"restrict_to_domain",
|
||||
"onboarding",
|
||||
"icon",
|
||||
"column_break_3",
|
||||
"extends_another_page",
|
||||
"is_standard",
|
||||
|
|
@ -22,6 +21,7 @@
|
|||
"disable_user_customization",
|
||||
"pin_to_top",
|
||||
"pin_to_bottom",
|
||||
"hide_custom",
|
||||
"section_break_2",
|
||||
"charts_label",
|
||||
"charts",
|
||||
|
|
@ -58,12 +58,6 @@
|
|||
"label": "Shortcuts",
|
||||
"options": "Desk Shortcut"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.extends_another_page == 0",
|
||||
"fieldname": "onboarding",
|
||||
"fieldtype": "Data",
|
||||
"label": "Onboarding"
|
||||
},
|
||||
{
|
||||
"fieldname": "restrict_to_domain",
|
||||
"fieldtype": "Link",
|
||||
|
|
@ -80,12 +74,6 @@
|
|||
"label": "Module",
|
||||
"options": "Module Def"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.extends_another_page == 0",
|
||||
"fieldname": "icon",
|
||||
"fieldtype": "Data",
|
||||
"label": "Icon"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
|
|
@ -183,7 +171,7 @@
|
|||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.extends_another_page == 1",
|
||||
"depends_on": "eval:doc.extends_another_page == 1 || doc.for_user",
|
||||
"fieldname": "extends",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
|
|
@ -196,10 +184,23 @@
|
|||
"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",
|
||||
"fieldname": "hide_custom",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Custom DocTypes and Reports"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-03-26 12:35:41.981432",
|
||||
"modified": "2020-05-18 19:17:27.206646",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Desk Page",
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@
|
|||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"type",
|
||||
"label",
|
||||
"column_break_4",
|
||||
"link_to",
|
||||
"column_break_4",
|
||||
"label",
|
||||
"icon",
|
||||
"restrict_to_domain",
|
||||
"section_break_5",
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Type",
|
||||
"options": "DocType\nReport\nPage",
|
||||
"options": "DocType\nReport\nPage\nDashboard",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -81,13 +81,14 @@
|
|||
{
|
||||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Label",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-07 19:04:23.645198",
|
||||
"modified": "2020-05-14 16:02:15.420993",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Desk Shortcut",
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ class TestEvent(unittest.TestCase):
|
|||
ev = frappe.get_doc(self.test_records[0]).insert()
|
||||
|
||||
add({
|
||||
"assign_to": "test@example.com",
|
||||
"assign_to": ["test@example.com"],
|
||||
"doctype": "Event",
|
||||
"name": ev.name,
|
||||
"description": "Test Assignment"
|
||||
|
|
@ -83,7 +83,7 @@ class TestEvent(unittest.TestCase):
|
|||
|
||||
# add another one
|
||||
add({
|
||||
"assign_to": self.test_user,
|
||||
"assign_to": [self.test_user],
|
||||
"doctype": "Event",
|
||||
"name": ev.name,
|
||||
"description": "Test Assignment"
|
||||
|
|
|
|||
0
frappe/desk/doctype/module_onboarding/__init__.py
Normal file
0
frappe/desk/doctype/module_onboarding/__init__.py
Normal file
27
frappe/desk/doctype/module_onboarding/module_onboarding.js
Normal file
27
frappe/desk/doctype/module_onboarding/module_onboarding.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright (c) 2020, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Module Onboarding", {
|
||||
refresh: function(frm) {
|
||||
frappe.boot.developer_mode &&
|
||||
frm.set_intro(
|
||||
__(
|
||||
"Saving this will export this document as well as the steps linked here as json."
|
||||
),
|
||||
true
|
||||
);
|
||||
if (!frappe.boot.developer_mode) {
|
||||
frm.trigger("disable_form");
|
||||
}
|
||||
},
|
||||
|
||||
disable_form: function(frm) {
|
||||
frm.set_read_only();
|
||||
frm.fields
|
||||
.filter((field) => field.has_input)
|
||||
.forEach((field) => {
|
||||
frm.set_df_property(field.df.fieldname, "read_only", "1");
|
||||
});
|
||||
frm.disable_save();
|
||||
},
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue