Merge branch 'develop' into paytm-integration

This commit is contained in:
Suraj Shetty 2020-05-30 21:08:47 +05:30 committed by GitHub
commit 06c54f5322
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
512 changed files with 266144 additions and 259520 deletions

View file

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

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

View file

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

View file

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

View file

@ -2,6 +2,6 @@
"baseUrl": "http://test_site_ui:8000",
"projectId": "92odwv",
"adminPassword": "admin",
"defaultCommandTimeout": 10000,
"defaultCommandTimeout": 20000,
"pageLoadTimeout": 15000
}

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,6 @@
context('Relative Timeframe', () => {
beforeEach(() => {
cy.login();
cy.visit('/desk#workspace/Website');
});
before(() => {
cy.login();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,5 @@
.restricted-button {
cursor: default;
position: relative;
right: -5px;
}

View file

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

View file

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

View file

@ -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 &lt;script&gt; or just characters like &lt; or &gt;, 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 &lt;script&gt; or just characters like &lt; or &gt;, 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",

View file

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

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

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

View file

@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class CSSClass(Document):
class CustomLink(Document):
pass

View file

@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
import unittest
class TestCSSClass(unittest.TestCase):
class TestCustomLink(unittest.TestCase):
pass

View file

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

View file

@ -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&gt;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&gt;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",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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