diff --git a/.travis.yml b/.travis.yml
index 30eb882256..a1568c9118 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -25,6 +25,7 @@ cache:
# https://docs.cypress.io/guides/guides/continuous-integration.html#Caching
- ~/.cache
+
matrix:
include:
- name: "Python 3.7 MariaDB"
@@ -46,7 +47,30 @@ matrix:
script: bench --site test_site run-ui-tests frappe --headless
before_install:
- # install wkhtmltopdf
+ # do we really want to run travis? check which files are changed and if git doesnt face any fatal errors
+ - |
+ FILES_CHANGED=$( git diff --name-only $TRAVIS_COMMIT_RANGE 2>&1 )
+
+ if [[ $FILES_CHANGED != *"fatal"* ]]; then
+ ONLY_DOCS_CHANGES=$( echo $FILES_CHANGED | grep -qvE '\.(md|png|jpg|jpeg)$|^.github|LICENSE' ; echo $? )
+ ONLY_JS_CHANGES=$( echo $FILES_CHANGED | grep -qvE '\.js$' ; echo $? )
+ ONLY_PY_CHANGES=$( echo $FILES_CHANGED | 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
+ 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
diff --git a/README.md b/README.md
index 860958087e..7545249610 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
@@ -33,8 +33,8 @@
Full-stack web application framework that uses Python and MariaDB on the server side and a tightly integrated client side library. Built for [ERPNext](https://erpnext.com)
### Table of Contents
-* [Installation](#installation)
-* [Documentation](https://frappe.io/docs)
+* [Installation](https://frappeframework.com/docs/user/en/installation)
+* [Documentation](https://frappeframework.com/docs)
* [License](#license)
### Installation
@@ -49,7 +49,7 @@ Full-stack web application framework that uses Python and MariaDB on the server
### Website
For details and documentation, see the website
-[https://frappe.io](https://frappe.io)
+[https://frappeframework.com](https://frappeframework.com)
### License
This repository has been released under the [MIT License](LICENSE).
diff --git a/cypress.json b/cypress.json
index ae0c45c3ae..97ac41bb61 100644
--- a/cypress.json
+++ b/cypress.json
@@ -2,6 +2,6 @@
"baseUrl": "http://test_site_ui:8000",
"projectId": "92odwv",
"adminPassword": "admin",
- "defaultCommandTimeout": 10000,
+ "defaultCommandTimeout": 20000,
"pageLoadTimeout": 15000
}
diff --git a/cypress/integration/control_duration.js b/cypress/integration/control_duration.js
new file mode 100644
index 0000000000..edad759216
--- /dev/null
+++ b/cypress/integration/control_duration.js
@@ -0,0 +1,45 @@
+context('Control Duration', () => {
+ before(() => {
+ cy.login();
+ cy.visit('/desk#workspace/Website');
+ });
+
+ function get_dialog_with_duration(hide_days=0, hide_seconds=0) {
+ return cy.dialog({
+ title: 'Duration',
+ fields: [{
+ 'fieldname': 'duration',
+ 'fieldtype': 'Duration',
+ 'hide_days': hide_days,
+ 'hide_seconds': hide_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(1, 1).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');
+ });
+});
\ No newline at end of file
diff --git a/cypress/integration/control_link.js b/cypress/integration/control_link.js
index 658a7fe320..0dc7d5b88e 100644
--- a/cypress/integration/control_link.js
+++ b/cypress/integration/control_link.js
@@ -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 });
diff --git a/cypress/integration/form.js b/cypress/integration/form.js
index 23fc57fc57..ef89a18e7d 100644
--- a/cypress/integration/form.js
+++ b/cypress/integration/form.js
@@ -9,6 +9,7 @@ context('Form', () => {
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.wait(300);
cy.get('.page-title').should('contain', 'Not Saved');
cy.server();
cy.route({
diff --git a/cypress/integration/grid_pagination.js b/cypress/integration/grid_pagination.js
index f03384cb93..b383f30bb8 100644
--- a/cypress/integration/grid_pagination.js
+++ b/cypress/integration/grid_pagination.js
@@ -40,12 +40,12 @@ context('Grid Pagination', () => {
cy.get('@table').find('.current-page-number').should('contain', '20');
cy.get('@table').find('.total-page-number').should('contain', '20');
});
- it('deletes all rows', ()=> {
- cy.visit('/desk#Form/Contact/Test Contact');
- cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
- cy.get('@table').find('.grid-heading-row .grid-row-check').click({force: true});
- cy.get('@table').find('button.grid-remove-all-rows').click();
- cy.get('.modal-dialog .btn-primary').contains('Yes').click();
- cy.get('@table').find('.grid-body .grid-row').should('have.length', 0);
- });
+ // it('deletes all rows', ()=> {
+ // cy.visit('/desk#Form/Contact/Test Contact');
+ // cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
+ // cy.get('@table').find('.grid-heading-row .grid-row-check').click({force: true});
+ // cy.get('@table').find('button.grid-remove-all-rows').click();
+ // cy.get('.modal-dialog .btn-primary').contains('Yes').click();
+ // cy.get('@table').find('.grid-body .grid-row').should('have.length', 0);
+ // });
});
\ No newline at end of file
diff --git a/cypress/integration/relative_filters.js b/cypress/integration/relative_time_filters.js
similarity index 85%
rename from cypress/integration/relative_filters.js
rename to cypress/integration/relative_time_filters.js
index 986c5ce342..ac70c44345 100644
--- a/cypress/integration/relative_filters.js
+++ b/cypress/integration/relative_time_filters.js
@@ -1,7 +1,6 @@
context('Relative Timeframe', () => {
beforeEach(() => {
cy.login();
- cy.visit('/desk#workspace/Website');
});
before(() => {
cy.login();
@@ -10,14 +9,14 @@ context('Relative Timeframe', () => {
frappe.call("frappe.tests.ui_test_helpers.create_todo_records");
});
});
- it('set relative filter for Previous and check list', () => {
+ it('sets relative timespan filter for last week and filters list', () => {
cy.visit('/desk#List/ToDo/List');
cy.get('.list-row:contains("this is fourth todo")').should('exist');
cy.get('.tag-filters-area .btn:contains("Add Filter")').click();
cy.get('.fieldname-select-area').should('exist');
cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
- cy.get('select.condition.form-control').select("Previous");
- cy.get('.filter-field select.input-with-feedback.form-control').select("1 week");
+ cy.get('select.condition.form-control').select("Timespan");
+ cy.get('.filter-field select.input-with-feedback.form-control').select("last week");
cy.server();
cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
cy.get('.filter-box .btn:contains("Apply")').click();
@@ -29,13 +28,13 @@ context('Relative Timeframe', () => {
cy.get('.remove-filter.btn').click();
cy.wait('@save_user_settings');
});
- it('set relative filter for Next and check list', () => {
+ it('sets relative timespan filter for next week and filters list', () => {
cy.visit('/desk#List/ToDo/List');
cy.get('.list-row:contains("this is fourth todo")').should('exist');
cy.get('.tag-filters-area .btn:contains("Add Filter")').click();
cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
- cy.get('select.condition.form-control').select("Next");
- cy.get('.filter-field select.input-with-feedback.form-control').select("1 week");
+ cy.get('select.condition.form-control').select("Timespan");
+ cy.get('.filter-field select.input-with-feedback.form-control').select("next week");
cy.server();
cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
cy.get('.filter-box .btn:contains("Apply")').click();
diff --git a/frappe/__init__.py b/frappe/__init__.py
index f0b6bfe41b..8f36c0c4d3 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -231,9 +231,8 @@ def get_site_config(sites_path=None, site_path=None):
if os.path.exists(site_config):
config.update(get_file_json(site_config))
elif local.site and not local.flags.new_site:
- print("{0} does not exist".format(local.site))
+ print("Site {0} does not exist".format(local.site))
sys.exit(1)
- #raise IncorrectSitePath, "{0} does not exist".format(site_config)
return _dict(config)
@@ -1559,10 +1558,10 @@ def get_doctype_app(doctype):
loggers = {}
log_level = None
-def logger(module=None, with_more_info=True):
+def logger(module=None, with_more_info=False):
'''Returns a python logger that uses StreamHandler'''
from frappe.utils.logger import get_logger
- return get_logger(module or 'default', with_more_info=with_more_info)
+ return get_logger(module=module, with_more_info=with_more_info)
def log_error(message=None, title=_("Error")):
'''Log error to Error Log'''
diff --git a/frappe/app.py b/frappe/app.py
index 3bb764149b..50d09177d6 100644
--- a/frappe/app.py
+++ b/frappe/app.py
@@ -99,6 +99,16 @@ def application(request):
frappe.monitor.stop(response)
frappe.recorder.dump()
+ frappe.logger("web").info({
+ "site": get_site_name(request.host),
+ "remote_addr": getattr(request, "remote_addr", "NOTFOUND"),
+ "base_url": getattr(request, "base_url", "NOTFOUND"),
+ "full_path": getattr(request, "full_path", "NOTFOUND"),
+ "method": getattr(request, "method", "NOTFOUND"),
+ "scheme": getattr(request, "scheme", "NOTFOUND"),
+ "http_status_code": getattr(response, "status_code", "NOTFOUND")
+ })
+
if response and hasattr(frappe.local, 'rate_limiter'):
response.headers.extend(frappe.local.rate_limiter.headers())
@@ -195,7 +205,6 @@ def handle_exception(e):
frappe.local.login_manager.clear_cookies()
if http_status_code >= 500:
- frappe.logger().error('Request Error', exc_info=True)
make_error_snapshot(e)
if return_as_message:
diff --git a/frappe/boot.py b/frappe/boot.py
index 0eb6265942..8862ce3c61 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -19,6 +19,7 @@ from frappe.email.inbox import get_email_accounts
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
+from frappe.model.base_document import get_controller
from frappe.social.doctype.post.post import frequently_visited_links
def get_bootinfo():
@@ -84,6 +85,7 @@ def get_bootinfo():
bootinfo.points = get_energy_points(frappe.session.user)
bootinfo.frequently_visited_links = frequently_visited_links()
bootinfo.link_preview_doctypes = get_link_preview_doctypes()
+ bootinfo.additional_filters_config = get_additional_filters_from_hooks()
return bootinfo
@@ -106,7 +108,8 @@ 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_list("Dashboard")
+ bootinfo.module_page_map = get_controller("Desk Page").get_module_page_map()
+ bootinfo.dashboards = frappe.get_all("Dashboard")
def get_allowed_pages(cache=False):
return get_user_pages_or_reports('Page', cache=cache)
@@ -295,3 +298,11 @@ def get_link_preview_doctypes():
link_preview_doctypes.append(custom.doc_type)
return link_preview_doctypes
+
+def get_additional_filters_from_hooks():
+ filter_config = frappe._dict()
+ filter_hooks = frappe.get_hooks('filters_config')
+ for hook in filter_hooks:
+ filter_config.update(frappe.get_attr(hook)())
+
+ return filter_config
diff --git a/frappe/commands/__init__.py b/frappe/commands/__init__.py
index 8110f2ec19..b7294fff77 100644
--- a/frappe/commands/__init__.py
+++ b/frappe/commands/__init__.py
@@ -22,7 +22,11 @@ def pass_context(f):
pr = cProfile.Profile()
pr.enable()
- ret = f(frappe._dict(ctx.obj), *args, **kwargs)
+ try:
+ ret = f(frappe._dict(ctx.obj), *args, **kwargs)
+ except frappe.exceptions.SiteNotSpecifiedError as e:
+ click.secho(str(e), fg='yellow')
+ sys.exit(1)
if profile:
pr.disable()
@@ -39,13 +43,14 @@ def pass_context(f):
return click.pass_context(_func)
-def get_site(context):
+def get_site(context, raise_err=True):
try:
site = context.sites[0]
return site
except (IndexError, TypeError):
- print('Please specify --site sitename')
- sys.exit(1)
+ if raise_err:
+ raise frappe.SiteNotSpecifiedError
+ return None
def popen(command, *args, **kwargs):
output = kwargs.get('output', True)
diff --git a/frappe/commands/scheduler.py b/frappe/commands/scheduler.py
index 6f51c81211..bd9c9d2cb0 100755
--- a/frappe/commands/scheduler.py
+++ b/frappe/commands/scheduler.py
@@ -4,6 +4,7 @@ import sys
import frappe
from frappe.utils import cint
from frappe.commands import pass_context, get_site
+from frappe.exceptions import SiteNotSpecifiedError
def _is_scheduler_enabled():
enable_scheduler = False
@@ -30,6 +31,8 @@ def trigger_scheduler_event(context, event):
frappe.utils.scheduler.trigger(site, event, now=True)
finally:
frappe.destroy()
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('enable-scheduler')
@pass_context
@@ -45,6 +48,8 @@ def enable_scheduler(context):
print("Enabled for", site)
finally:
frappe.destroy()
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('disable-scheduler')
@pass_context
@@ -60,7 +65,8 @@ def disable_scheduler(context):
print("Disabled for", site)
finally:
frappe.destroy()
-
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('scheduler')
@@ -120,7 +126,7 @@ def doctor(context, site=None):
"Get diagnostic info about background workers"
from frappe.utils.doctor import doctor as _doctor
if not site:
- site = get_site(context)
+ site = get_site(context, raise_err=False)
return _doctor(site=site)
@click.command('show-pending-jobs')
diff --git a/frappe/commands/site.py b/frappe/commands/site.py
index 82ed72dd5c..28e61282eb 100755
--- a/frappe/commands/site.py
+++ b/frappe/commands/site.py
@@ -15,6 +15,7 @@ import frappe
from frappe import _
from frappe.commands import get_site, pass_context
from frappe.commands.scheduler import _is_scheduler_enabled
+from frappe.exceptions import SiteNotSpecifiedError
from frappe.installer import update_site_config
from frappe.utils import get_site_path, touch_file
@@ -43,14 +44,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,7 +82,6 @@ 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,
admin_password=admin_password, verbose=verbose, source_sql=source_sql, force=force, reinstall=reinstall,
@@ -96,15 +98,6 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
scheduler_status = "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled"
print("*** Scheduler is", scheduler_status, "***")
-def _new_site_cleanup(site, mariadb_root_username, mariadb_root_password):
- installing = get_site_path('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, no_backup=True)
- shutil.rmtree(site)
-
- frappe.destroy()
@click.command('restore')
@click.argument('sql-file-path')
@@ -122,30 +115,47 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
# Extract the gzip file if user has passed *.sql.gz file instead of *.sql file
if not os.path.exists(sql_file_path):
- sql_file_path = '../' + sql_file_path
+ base_path = '..'
+ sql_file_path = os.path.join(base_path, sql_file_path)
if not os.path.exists(sql_file_path):
print('Invalid path {0}'.format(sql_file_path[3:]))
sys.exit(1)
+ elif sql_file_path.startswith(os.sep):
+ base_path = os.sep
+ else:
+ base_path = '.'
+
if sql_file_path.endswith('sql.gz'):
- sql_file_path = extract_sql_gzip(os.path.abspath(sql_file_path))
+ decompressed_file_name = extract_sql_gzip(os.path.abspath(sql_file_path))
+ else:
+ decompressed_file_name = sql_file_path
site = get_site(context)
frappe.init(site=site)
_new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username,
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
- verbose=context.verbose, install_apps=install_app, source_sql=sql_file_path,
- force=context.force)
+ verbose=context.verbose, install_apps=install_app, source_sql=decompressed_file_name,
+ force=True)
# Extract public and/or private files to the restored site, if user has given the path
if with_public_files:
+ with_public_files = os.path.join(base_path, with_public_files)
public = extract_tar_files(site, with_public_files, 'public')
os.remove(public)
if with_private_files:
+ with_private_files = os.path.join(base_path, with_private_files)
private = extract_tar_files(site, with_private_files, 'private')
os.remove(private)
+ # Removing temporarily created file
+ if decompressed_file_name != sql_file_path:
+ os.remove(decompressed_file_name)
+
+ success_message = "Site {0} has been restored{1}".format(site, " with files" if (with_public_files or with_private_files) else "")
+ click.secho(success_message, fg="green")
+
@click.command('reinstall')
@click.option('--admin-password', help='Administrator Password for reinstalled site')
@click.option('--mariadb-root-username', help='Root username for MariaDB')
@@ -192,6 +202,8 @@ def install_app(context, apps):
_install_app(app, verbose=context.verbose)
finally:
frappe.destroy()
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('list-apps')
@pass_context
@@ -221,7 +233,8 @@ def add_system_manager(context, email, first_name, last_name, send_welcome_email
frappe.db.commit()
finally:
frappe.destroy()
-
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('disable-user')
@click.argument('email')
@@ -252,6 +265,8 @@ def migrate(context, rebuild_website=False, skip_failing=False):
migrate(context.verbose, rebuild_website=rebuild_website, skip_failing=skip_failing)
finally:
frappe.destroy()
+ if not context.sites:
+ raise SiteNotSpecifiedError
print("Compiling Python Files...")
compileall.compile_dir('../apps', quiet=1, rx=re.compile('.*node_modules.*'))
@@ -263,7 +278,12 @@ 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:
+ frappe.init(site=site)
+ frappe.connect()
migrate_to(site, frappe_provider)
+ frappe.destroy()
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('run-patch')
@click.argument('module')
@@ -278,6 +298,8 @@ def run_patch(context, module):
frappe.modules.patch_handler.run_single(module, force=context.force)
finally:
frappe.destroy()
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('reload-doc')
@click.argument('module')
@@ -294,6 +316,8 @@ def reload_doc(context, module, doctype, docname):
frappe.db.commit()
finally:
frappe.destroy()
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('reload-doctype')
@click.argument('doctype')
@@ -308,6 +332,8 @@ def reload_doctype(context, doctype):
frappe.db.commit()
finally:
frappe.destroy()
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('add-to-hosts')
@pass_context
@@ -315,6 +341,8 @@ def add_to_hosts(context):
"Add site to hosts"
for site in context.sites:
frappe.commands.popen('echo 127.0.0.1\t{0} | sudo tee -a /etc/hosts'.format(site))
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('use')
@click.argument('site')
@@ -328,7 +356,7 @@ def use(site, sites_path='.'):
sitefile.write(site)
print("Current Site set to {}".format(site))
else:
- print("{} does not exist".format(site))
+ print("Site {} does not exist".format(site))
@click.command('backup')
@click.option('--with-files', default=False, is_flag=True, help="Take backup with files")
@@ -361,6 +389,9 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non
print("Private files: ", odb.backup_path_private_files)
frappe.destroy()
+ if not context.sites:
+ raise SiteNotSpecifiedError
+
sys.exit(exit_code)
@click.command('remove-from-installed-apps')
@@ -376,6 +407,8 @@ def remove_from_installed_apps(context, app):
remove_from_installed_apps(app)
finally:
frappe.destroy()
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('uninstall-app')
@click.argument('app')
@@ -392,6 +425,8 @@ def uninstall(context, app, dry_run=False, yes=False):
remove_app(app, dry_run, yes)
finally:
frappe.destroy()
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('drop-site')
@@ -422,7 +457,7 @@ def _drop_site(site, root_login='root', root_password=None, archived_sites_path=
else:
click.echo("="*80)
click.echo("Error: The operation has stopped because backup of {s}'s database failed.".format(s=site))
- click.echo("Reason: {reason}{sep}".format(reason=err[1], sep="\n"))
+ click.echo("Reason: {reason}{sep}".format(reason=str(err), sep="\n"))
click.echo("Fix the issue and try again.")
click.echo(
"Hint: Use 'bench drop-site {s} --force' to force the removal of {s}".format(sep="\n", tab="\t", s=site)
@@ -483,6 +518,8 @@ def set_admin_password(context, admin_password, logout_all_sessions=False):
admin_password = None
finally:
frappe.destroy()
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('set-last-active-for-user')
@click.option('--user', help="Setup last active date for user")
@@ -528,6 +565,8 @@ def publish_realtime(context, event, message, room, user, doctype, docname, afte
frappe.db.commit()
finally:
frappe.destroy()
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('browse')
@click.argument('site', required=False)
@@ -555,6 +594,8 @@ def start_recording(context):
for site in context.sites:
frappe.init(site=site)
frappe.recorder.start()
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('stop-recording')
@@ -563,6 +604,8 @@ def stop_recording(context):
for site in context.sites:
frappe.init(site=site)
frappe.recorder.stop()
+ if not context.sites:
+ raise SiteNotSpecifiedError
commands = [
diff --git a/frappe/commands/translate.py b/frappe/commands/translate.py
index 5a48e2b409..48a7fd1db7 100644
--- a/frappe/commands/translate.py
+++ b/frappe/commands/translate.py
@@ -1,6 +1,7 @@
from __future__ import unicode_literals, absolute_import, print_function
import click
from frappe.commands import pass_context, get_site
+from frappe.exceptions import SiteNotSpecifiedError
# translation
@click.command('build-message-files')
@@ -15,6 +16,8 @@ def build_message_files(context):
frappe.translate.rebuild_all_translation_files()
finally:
frappe.destroy()
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('new-language') #, help="Create lang-code.csv for given app")
@pass_context
diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py
index 3610393d9a..343dc6e2bc 100644
--- a/frappe/commands/utils.py
+++ b/frappe/commands/utils.py
@@ -6,6 +6,7 @@ import json, os, sys, subprocess
from distutils.spawn import find_executable
import frappe
from frappe.commands import pass_context, get_site
+from frappe.exceptions import SiteNotSpecifiedError
from frappe.utils import update_progress_bar, get_bench_path
from frappe.utils.response import json_handler
from coverage import Coverage
@@ -51,7 +52,8 @@ def clear_cache(context):
frappe.website.render.clear_cache()
finally:
frappe.destroy()
-
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('clear-website-cache')
@pass_context
@@ -65,7 +67,8 @@ def clear_website_cache(context):
frappe.website.render.clear_cache()
finally:
frappe.destroy()
-
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('destroy-all-sessions')
@click.option('--reason')
@@ -81,7 +84,8 @@ def destroy_all_sessions(context, reason=None):
frappe.db.commit()
finally:
frappe.destroy()
-
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('show-config')
@pass_context
@@ -117,7 +121,8 @@ def reset_perms(context):
reset_perms(d)
finally:
frappe.destroy()
-
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('execute')
@click.argument('method')
@@ -164,6 +169,9 @@ def execute(context, method, args=None, kwargs=None, profile=False):
if ret:
print(json.dumps(ret, default=json_handler))
+ if not context.sites:
+ raise SiteNotSpecifiedError
+
@click.command('add-to-email-queue')
@click.argument('email-path')
@@ -197,7 +205,8 @@ def export_doc(context, doctype, docname):
frappe.modules.export_doc(doctype, docname)
finally:
frappe.destroy()
-
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('export-json')
@click.argument('doctype')
@@ -214,7 +223,8 @@ def export_json(context, doctype, path, name=None):
data_import.export_json(doctype, path, name=name)
finally:
frappe.destroy()
-
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('export-csv')
@click.argument('doctype')
@@ -230,7 +240,8 @@ def export_csv(context, doctype, path):
data_import.export_csv(doctype, path)
finally:
frappe.destroy()
-
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('export-fixtures')
@click.option('--app', default=None, help='Export fixtures of a specific app')
@@ -245,7 +256,8 @@ def export_fixtures(context, app=None):
export_fixtures(app=app)
finally:
frappe.destroy()
-
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('import-doc')
@click.argument('path')
@@ -267,7 +279,8 @@ def import_doc(context, path, force=False):
data_import.import_doc(path, overwrite=context.force)
finally:
frappe.destroy()
-
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('import-csv')
@click.argument('path')
@@ -364,6 +377,8 @@ def mariadb(context):
import os
site = get_site(context)
+ if not site:
+ raise SiteNotSpecifiedError
frappe.init(site=site)
# This is assuming you're within the bench instance.
@@ -487,7 +502,17 @@ def run_tests(context, app=None, module=None, doctype=None, test=(),
if coverage:
# Generate coverage report only for app that is being tested
source_path = os.path.join(get_bench_path(), 'apps', app or 'frappe')
- cov = Coverage(source=[source_path], omit=['*.html', '*.js', '*.xml', '*.css', '*/doctype/*/*_dashboard.py', '*/patches/*'])
+ cov = Coverage(source=[source_path], omit=[
+ '*.html',
+ '*.js',
+ '*.xml',
+ '*.css',
+ '*.less',
+ '*.scss',
+ '*.vue',
+ '*/doctype/*/*_dashboard.py',
+ '*/patches/*'
+ ])
cov.start()
ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests,
@@ -577,7 +602,8 @@ def request(context, args=None, path=None):
print(frappe.response)
finally:
frappe.destroy()
-
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('make-app')
@click.argument('destination')
@@ -658,7 +684,8 @@ def rebuild_global_search(context, static_pages=False):
finally:
frappe.destroy()
-
+ if not context.sites:
+ raise SiteNotSpecifiedError
@click.command('auto-deploy')
@click.argument('app')
diff --git a/frappe/core/doctype/access_log/test_access_log.py b/frappe/core/doctype/access_log/test_access_log.py
index 312f77c026..9830507423 100644
--- a/frappe/core/doctype/access_log/test_access_log.py
+++ b/frappe/core/doctype/access_log/test_access_log.py
@@ -158,11 +158,7 @@ class TestAccessLog(unittest.TestCase):
request = requests.post(private_file_link, headers=self.header)
last_doc = frappe.get_last_doc('Access Log')
- if request.status_code == 403:
- # if file is not accessible, access log wont be generated
- pass
-
- else:
+ if request.ok:
# check for the access log of downloaded file
self.assertEqual(new_private_file.doctype, last_doc.export_from)
self.assertEqual(new_private_file.name, last_doc.reference_document)
diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py
index abd24fb468..232d485f36 100644
--- a/frappe/core/doctype/communication/communication.py
+++ b/frappe/core/doctype/communication/communication.py
@@ -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,63 @@ 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")
+ parent.run_method("handle_hold_time", "Replied")
+ apply_assignment_rule(parent)
+ else:
+ # update the modified date for document
+ parent.update_modified()
+
+ update_mins_to_first_communication(parent, doc)
+ set_avg_response_time(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)
+
+def set_avg_response_time(parent, communication):
+ if parent.meta.has_field("avg_response_time") and communication.sent_or_received == "Sent":
+ # avg response time for all the responses
+ communications = frappe.get_list("Communication", filters={
+ "reference_doctype": parent.doctype,
+ "reference_name": parent.name
+ },
+ fields=["sent_or_received", "name", "creation"],
+ order_by="creation"
+ )
+
+ if len(communications):
+ response_times = []
+ for i in range(len(communications)):
+ if communications[i].sent_or_received == "Sent" and communications[i-1].sent_or_received == "Received":
+ response_time = round(time_diff_in_seconds(communications[i].creation, communications[i-1].creation), 2)
+ if response_time > 0:
+ response_times.append(response_time)
+ if response_times:
+ avg_response_time = sum(response_times) / len(response_times)
+ parent.db_set("avg_response_time", avg_response_time)
\ No newline at end of file
diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py
index 8793c60934..daf64d4b8b 100755
--- a/frappe/core/doctype/communication/email.py
+++ b/frappe/core/doctype/communication/email.py
@@ -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:
diff --git a/frappe/core/doctype/communication/test_communication.py b/frappe/core/doctype/communication/test_communication.py
index fb859586bb..6df90baaae 100644
--- a/frappe/core/doctype/communication/test_communication.py
+++ b/frappe/core/doctype/communication/test_communication.py
@@ -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
diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json
index 8e7516cd0a..aab59a5a0a 100644
--- a/frappe/core/doctype/docfield/docfield.json
+++ b/frappe/core/doctype/docfield/docfield.json
@@ -13,6 +13,8 @@
"fieldname",
"precision",
"length",
+ "hide_days",
+ "hide_seconds",
"reqd",
"search_index",
"in_list_view",
@@ -87,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
},
@@ -450,6 +452,20 @@
"fieldname": "column_break_38",
"fieldtype": "Column Break"
},
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_days",
+ "fieldtype": "Check",
+ "label": "Hide Days"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_seconds",
+ "fieldtype": "Check",
+ "label": "Hide Seconds"
+ },
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Section Break'",
@@ -461,7 +477,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-04-27 11:38:21.223185",
+ "modified": "2020-02-06 09:06:25.224413",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 904deb9990..6ca3cccdba 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -688,6 +688,9 @@ def validate_fields(meta):
def check_link_table_options(docname, d):
if frappe.flags.in_patch: return
+
+ if frappe.flags.in_fixtures: return
+
if d.fieldtype in ("Link",) + table_fields:
if not d.options:
frappe.throw(_("{0}: Options required for Link or Table type field {1} in row {2}").format(docname, d.label, d.idx), DoctypeLinkError)
@@ -908,6 +911,8 @@ 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 frappe.flags.in_fixtures: return
if docfield.fieldtype not in ['Table MultiSelect', 'Table']: return
doctype = docfield.options
diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py
index b35abfa861..831d2ab22d 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -48,6 +48,8 @@ class File(Document):
def before_insert(self):
frappe.local.rollback_observers.append(self)
self.set_folder_name()
+ if self.file_name:
+ self.file_name = re.sub(r'/', '', self.file_name)
self.content = self.get("content", None)
self.decode = self.get("decode", False)
if self.content:
@@ -180,11 +182,11 @@ class File(Document):
if duplicate_file:
duplicate_file_doc = frappe.get_cached_doc('File', duplicate_file.name)
if duplicate_file_doc.exists_on_disk():
- # if it is attached to a document then throw DuplicateEntryError
+ # if it is attached to a document then throw FileAlreadyAttachedException
if self.attached_to_doctype and self.attached_to_name:
self.duplicate_entry = duplicate_file.name
frappe.throw(_("Same file has already been attached to the record"),
- frappe.DuplicateEntryError)
+ frappe.FileAlreadyAttachedException)
# else just use the url, to avoid uploading a duplicate
else:
self.file_url = duplicate_file.file_url
@@ -192,6 +194,8 @@ class File(Document):
def set_file_name(self):
if not self.file_name and self.file_url:
self.file_name = self.file_url.split('/')[-1]
+ else:
+ self.file_name = re.sub(r'/', '', self.file_name)
def generate_content_hash(self):
if self.content_hash or not self.file_url or self.file_url.startswith('http'):
@@ -405,6 +409,12 @@ class File(Document):
frappe.throw(_("URL must start with 'http://' or 'https://'"))
return
+ if not self.file_url.startswith(("http://", "https://")):
+ # local file
+ root_files_path = get_files_path(is_private=self.is_private)
+ if not os.path.commonpath([root_files_path]) == os.path.commonpath([root_files_path, self.get_full_path()]):
+ # basically the file url is skewed to not point to /files/ or /private/files
+ frappe.throw(_("{0} is not a valid file url").format(self.file_url))
self.file_url = unquote(self.file_url)
self.file_size = frappe.form_dict.file_size or self.file_size
@@ -704,7 +714,12 @@ def remove_all(dt, dn, from_delete=False):
try:
for fid in frappe.db.sql_list("""select name from `tabFile` where
attached_to_doctype=%s and attached_to_name=%s""", (dt, dn)):
- remove_file(fid=fid, attached_to_doctype=dt, attached_to_name=dn, from_delete=from_delete)
+ if from_delete:
+ # If deleting a doc, directly delete files
+ frappe.delete_doc("File", fid, ignore_permissions=True)
+ else:
+ # Removes file and adds a comment in the document it is attached to
+ remove_file(fid=fid, attached_to_doctype=dt, attached_to_name=dn, from_delete=from_delete)
except Exception as e:
if e.args[0]!=1054: raise # (temp till for patched)
diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
index c179054550..765ae5fe93 100644
--- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
+++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
@@ -84,7 +84,7 @@ class ScheduledJobType(Document):
def log_status(self, status):
# log file
- frappe.logger(__name__).info('Scheduled Job {0}: {1} for {2}'.format(status, self.method, frappe.local.site))
+ frappe.logger("scheduler").info('Scheduled Job {0}: {1} for {2}'.format(status, self.method, frappe.local.site))
self.update_scheduler_log(status)
def update_scheduler_log(self, status):
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 0c5ebc3ede..7b9266ff64 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -4,7 +4,7 @@
from __future__ import unicode_literals, print_function
import frappe
from frappe.model.document import Document
-from frappe.utils import cint, has_gravatar, format_datetime, now_datetime, get_formatted_email, today
+from frappe.utils import cint, flt, has_gravatar, format_datetime, now_datetime, get_formatted_email, today
from frappe import throw, msgprint, _
from frappe.utils.password import update_password as _update_password
from frappe.desk.notifications import clear_notifications
@@ -841,11 +841,11 @@ def user_query(doctype, txt, searchfield, start, page_len, filters):
def get_total_users():
"""Returns total no. of system users"""
- return frappe.db.sql('''SELECT SUM(`simultaneous_sessions`)
+ return flt(frappe.db.sql('''SELECT SUM(`simultaneous_sessions`)
FROM `tabUser`
WHERE `enabled` = 1
AND `user_type` = 'System User'
- AND `name` NOT IN ({})'''.format(", ".join(["%s"]*len(STANDARD_USERS))), STANDARD_USERS)[0][0]
+ AND `name` NOT IN ({})'''.format(", ".join(["%s"]*len(STANDARD_USERS))), STANDARD_USERS)[0][0])
def get_system_users(exclude_users=None, limit=None):
if not exclude_users:
diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json
index 122e6c7070..6fa7b29161 100644
--- a/frappe/custom/doctype/custom_field/custom_field.json
+++ b/frappe/custom/doctype/custom_field/custom_field.json
@@ -16,6 +16,8 @@
"column_break_6",
"fieldtype",
"precision",
+ "hide_seconds",
+ "hide_days",
"options",
"fetch_from",
"fetch_if_empty",
@@ -56,368 +58,382 @@
],
"fields": [
{
- "bold": 1,
- "fieldname": "dt",
- "fieldtype": "Link",
- "in_filter": 1,
- "in_list_view": 1,
- "label": "Document",
- "oldfieldname": "dt",
- "oldfieldtype": "Link",
- "options": "DocType",
- "reqd": 1,
- "search_index": 1
+ "bold": 1,
+ "fieldname": "dt",
+ "fieldtype": "Link",
+ "in_filter": 1,
+ "in_list_view": 1,
+ "label": "Document",
+ "oldfieldname": "dt",
+ "oldfieldtype": "Link",
+ "options": "DocType",
+ "reqd": 1,
+ "search_index": 1
},
{
- "bold": 1,
- "fieldname": "label",
- "fieldtype": "Data",
- "in_filter": 1,
- "label": "Label",
- "no_copy": 1,
- "oldfieldname": "label",
- "oldfieldtype": "Data"
+ "bold": 1,
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "in_filter": 1,
+ "label": "Label",
+ "no_copy": 1,
+ "oldfieldname": "label",
+ "oldfieldtype": "Data"
},
{
- "fieldname": "label_help",
- "fieldtype": "HTML",
- "label": "Label Help",
- "oldfieldtype": "HTML"
+ "fieldname": "label_help",
+ "fieldtype": "HTML",
+ "label": "Label Help",
+ "oldfieldtype": "HTML"
},
{
- "fieldname": "fieldname",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Fieldname",
- "no_copy": 1,
- "oldfieldname": "fieldname",
- "oldfieldtype": "Data",
- "read_only": 1
+ "fieldname": "fieldname",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Fieldname",
+ "no_copy": 1,
+ "oldfieldname": "fieldname",
+ "oldfieldtype": "Data",
+ "read_only": 1
},
{
- "description": "Select the label after which you want to insert new field.",
- "fieldname": "insert_after",
- "fieldtype": "Select",
- "label": "Insert After",
- "no_copy": 1,
- "oldfieldname": "insert_after",
- "oldfieldtype": "Select"
+ "description": "Select the label after which you want to insert new field.",
+ "fieldname": "insert_after",
+ "fieldtype": "Select",
+ "label": "Insert After",
+ "no_copy": 1,
+ "oldfieldname": "insert_after",
+ "oldfieldtype": "Select"
},
{
- "fieldname": "column_break_6",
- "fieldtype": "Column Break"
+ "fieldname": "column_break_6",
+ "fieldtype": "Column Break"
},
{
- "bold": 1,
- "default": "Data",
- "fieldname": "fieldtype",
- "fieldtype": "Select",
- "in_filter": 1,
- "in_list_view": 1,
- "label": "Field Type",
- "oldfieldname": "fieldtype",
- "oldfieldtype": "Select",
- "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
- "reqd": 1
+ "bold": 1,
+ "default": "Data",
+ "fieldname": "fieldtype",
+ "fieldtype": "Select",
+ "in_filter": 1,
+ "in_list_view": 1,
+ "label": "Field Type",
+ "oldfieldname": "fieldtype",
+ "oldfieldtype": "Select",
+ "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
+ "reqd": 1
},
{
- "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
- "description": "Set non-standard precision for a Float or Currency field",
- "fieldname": "precision",
- "fieldtype": "Select",
- "label": "Precision",
- "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
+ "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
+ "description": "Set non-standard precision for a Float or Currency field",
+ "fieldname": "precision",
+ "fieldtype": "Select",
+ "label": "Precision",
+ "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
},
{
- "fieldname": "options",
- "fieldtype": "Small Text",
- "in_list_view": 1,
- "label": "Options",
- "oldfieldname": "options",
- "oldfieldtype": "Text"
+ "fieldname": "options",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Options",
+ "oldfieldname": "options",
+ "oldfieldtype": "Text"
},
{
- "fieldname": "fetch_from",
- "fieldtype": "Small Text",
- "label": "Fetch From"
+ "fieldname": "fetch_from",
+ "fieldtype": "Small Text",
+ "label": "Fetch From"
},
{
- "default": "0",
- "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
- "fieldname": "fetch_if_empty",
- "fieldtype": "Check",
- "label": "Fetch If Empty"
+ "default": "0",
+ "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
+ "fieldname": "fetch_if_empty",
+ "fieldtype": "Check",
+ "label": "Fetch If Empty"
},
{
- "fieldname": "options_help",
- "fieldtype": "HTML",
- "label": "Options Help",
- "oldfieldtype": "HTML"
+ "fieldname": "options_help",
+ "fieldtype": "HTML",
+ "label": "Options Help",
+ "oldfieldtype": "HTML"
},
{
- "fieldname": "section_break_11",
- "fieldtype": "Section Break"
+ "fieldname": "section_break_11",
+ "fieldtype": "Section Break"
},
{
- "default": "0",
- "depends_on": "eval:doc.fieldtype==\"Section Break\"",
- "fieldname": "collapsible",
- "fieldtype": "Check",
- "label": "Collapsible"
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype==\"Section Break\"",
+ "fieldname": "collapsible",
+ "fieldtype": "Check",
+ "label": "Collapsible"
},
{
- "depends_on": "eval:doc.fieldtype==\"Section Break\"",
- "fieldname": "collapsible_depends_on",
- "fieldtype": "Code",
- "label": "Collapsible Depends On"
+ "depends_on": "eval:doc.fieldtype==\"Section Break\"",
+ "fieldname": "collapsible_depends_on",
+ "fieldtype": "Code",
+ "label": "Collapsible Depends On"
},
{
- "fieldname": "default",
- "fieldtype": "Text",
- "label": "Default Value",
- "oldfieldname": "default",
- "oldfieldtype": "Text"
+ "fieldname": "default",
+ "fieldtype": "Text",
+ "label": "Default Value",
+ "oldfieldname": "default",
+ "oldfieldtype": "Text"
},
{
- "fieldname": "depends_on",
- "fieldtype": "Code",
- "label": "Depends On",
- "length": 255
+ "fieldname": "depends_on",
+ "fieldtype": "Code",
+ "label": "Depends On",
+ "length": 255
},
{
- "fieldname": "description",
- "fieldtype": "Text",
- "label": "Field Description",
- "oldfieldname": "description",
- "oldfieldtype": "Text",
- "print_width": "300px",
- "width": "300px"
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "label": "Field Description",
+ "oldfieldname": "description",
+ "oldfieldtype": "Text",
+ "print_width": "300px",
+ "width": "300px"
},
{
- "default": "0",
- "fieldname": "permlevel",
- "fieldtype": "Int",
- "label": "Permission Level",
- "oldfieldname": "permlevel",
- "oldfieldtype": "Int"
+ "default": "0",
+ "fieldname": "permlevel",
+ "fieldtype": "Int",
+ "label": "Permission Level",
+ "oldfieldname": "permlevel",
+ "oldfieldtype": "Int"
},
{
- "fieldname": "width",
- "fieldtype": "Data",
- "label": "Width",
- "oldfieldname": "width",
- "oldfieldtype": "Data"
+ "fieldname": "width",
+ "fieldtype": "Data",
+ "label": "Width",
+ "oldfieldname": "width",
+ "oldfieldtype": "Data"
},
{
- "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
- "fieldname": "columns",
- "fieldtype": "Int",
- "label": "Columns"
+ "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
+ "fieldname": "columns",
+ "fieldtype": "Int",
+ "label": "Columns"
},
{
- "fieldname": "properties",
- "fieldtype": "Column Break",
- "oldfieldtype": "Column Break",
- "print_width": "50%",
- "width": "50%"
+ "fieldname": "properties",
+ "fieldtype": "Column Break",
+ "oldfieldtype": "Column Break",
+ "print_width": "50%",
+ "width": "50%"
},
{
- "default": "0",
- "fieldname": "reqd",
- "fieldtype": "Check",
- "in_list_view": 1,
- "label": "Is Mandatory Field",
- "oldfieldname": "reqd",
- "oldfieldtype": "Check"
+ "default": "0",
+ "fieldname": "reqd",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Is Mandatory Field",
+ "oldfieldname": "reqd",
+ "oldfieldtype": "Check"
},
{
- "default": "0",
- "fieldname": "unique",
- "fieldtype": "Check",
- "label": "Unique"
+ "default": "0",
+ "fieldname": "unique",
+ "fieldtype": "Check",
+ "label": "Unique"
},
{
- "default": "0",
- "fieldname": "read_only",
- "fieldtype": "Check",
- "label": "Read Only"
+ "default": "0",
+ "fieldname": "read_only",
+ "fieldtype": "Check",
+ "label": "Read Only"
},
{
- "default": "0",
- "depends_on": "eval:doc.fieldtype===\"Link\"",
- "fieldname": "ignore_user_permissions",
- "fieldtype": "Check",
- "label": "Ignore User Permissions"
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype===\"Link\"",
+ "fieldname": "ignore_user_permissions",
+ "fieldtype": "Check",
+ "label": "Ignore User Permissions"
},
{
- "default": "0",
- "fieldname": "hidden",
- "fieldtype": "Check",
- "label": "Hidden"
+ "default": "0",
+ "fieldname": "hidden",
+ "fieldtype": "Check",
+ "label": "Hidden"
},
{
- "default": "0",
- "fieldname": "print_hide",
- "fieldtype": "Check",
- "label": "Print Hide",
- "oldfieldname": "print_hide",
- "oldfieldtype": "Check"
+ "default": "0",
+ "fieldname": "print_hide",
+ "fieldtype": "Check",
+ "label": "Print Hide",
+ "oldfieldname": "print_hide",
+ "oldfieldtype": "Check"
},
{
- "default": "0",
- "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
- "fieldname": "print_hide_if_no_value",
- "fieldtype": "Check",
- "label": "Print Hide If No Value"
+ "default": "0",
+ "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
+ "fieldname": "print_hide_if_no_value",
+ "fieldtype": "Check",
+ "label": "Print Hide If No Value"
},
{
- "fieldname": "print_width",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Print Width",
- "no_copy": 1,
- "print_hide": 1
+ "fieldname": "print_width",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Print Width",
+ "no_copy": 1,
+ "print_hide": 1
},
{
- "default": "0",
- "fieldname": "no_copy",
- "fieldtype": "Check",
- "label": "No Copy",
- "oldfieldname": "no_copy",
- "oldfieldtype": "Check"
+ "default": "0",
+ "fieldname": "no_copy",
+ "fieldtype": "Check",
+ "label": "No Copy",
+ "oldfieldname": "no_copy",
+ "oldfieldtype": "Check"
},
{
- "default": "0",
- "fieldname": "allow_on_submit",
- "fieldtype": "Check",
- "label": "Allow on Submit",
- "oldfieldname": "allow_on_submit",
- "oldfieldtype": "Check"
+ "default": "0",
+ "fieldname": "allow_on_submit",
+ "fieldtype": "Check",
+ "label": "Allow on Submit",
+ "oldfieldname": "allow_on_submit",
+ "oldfieldtype": "Check"
},
{
- "default": "0",
- "fieldname": "in_list_view",
- "fieldtype": "Check",
- "label": "In List View"
+ "default": "0",
+ "fieldname": "in_list_view",
+ "fieldtype": "Check",
+ "label": "In List View"
},
{
- "default": "0",
- "fieldname": "in_standard_filter",
- "fieldtype": "Check",
- "label": "In Standard Filter"
+ "default": "0",
+ "fieldname": "in_standard_filter",
+ "fieldtype": "Check",
+ "label": "In Standard Filter"
},
{
- "default": "0",
- "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
- "fieldname": "in_global_search",
- "fieldtype": "Check",
- "label": "In Global Search"
+ "default": "0",
+ "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
+ "fieldname": "in_global_search",
+ "fieldtype": "Check",
+ "label": "In Global Search"
},
{
- "default": "0",
- "fieldname": "bold",
- "fieldtype": "Check",
- "label": "Bold"
+ "default": "0",
+ "fieldname": "bold",
+ "fieldtype": "Check",
+ "label": "Bold"
},
{
- "default": "0",
- "fieldname": "report_hide",
- "fieldtype": "Check",
- "label": "Report Hide",
- "oldfieldname": "report_hide",
- "oldfieldtype": "Check"
+ "default": "0",
+ "fieldname": "report_hide",
+ "fieldtype": "Check",
+ "label": "Report Hide",
+ "oldfieldname": "report_hide",
+ "oldfieldtype": "Check"
},
{
- "default": "0",
- "fieldname": "search_index",
- "fieldtype": "Check",
- "hidden": 1,
- "label": "Index",
- "no_copy": 1,
- "print_hide": 1
+ "default": "0",
+ "fieldname": "search_index",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Index",
+ "no_copy": 1,
+ "print_hide": 1
},
{
- "default": "0",
- "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
- "fieldname": "ignore_xss_filter",
- "fieldtype": "Check",
- "label": "Ignore XSS Filter"
+ "default": "0",
+ "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
+ "fieldname": "ignore_xss_filter",
+ "fieldtype": "Check",
+ "label": "Ignore XSS Filter"
},
{
- "default": "1",
- "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
- "fieldname": "translatable",
- "fieldtype": "Check",
- "label": "Translatable"
+ "default": "1",
+ "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
+ "fieldname": "translatable",
+ "fieldtype": "Check",
+ "label": "Translatable"
},
{
- "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
- "fieldname": "length",
- "fieldtype": "Int",
- "label": "Length"
+ "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
+ "fieldname": "length",
+ "fieldtype": "Int",
+ "label": "Length"
},
{
- "fieldname": "mandatory_depends_on",
- "fieldtype": "Code",
- "label": "Mandatory Depends On",
- "length": 255
+ "fieldname": "mandatory_depends_on",
+ "fieldtype": "Code",
+ "label": "Mandatory Depends On",
+ "length": 255
},
{
- "fieldname": "read_only_depends_on",
- "fieldtype": "Code",
- "label": "Read Only Depends On",
- "length": 255
+ "fieldname": "read_only_depends_on",
+ "fieldtype": "Code",
+ "label": "Read Only Depends On",
+ "length": 255
},
{
- "default": "0",
- "fieldname": "allow_in_quick_entry",
- "fieldtype": "Check",
- "label": "Allow in Quick Entry"
+ "default": "0",
+ "fieldname": "allow_in_quick_entry",
+ "fieldtype": "Check",
+ "label": "Allow in Quick Entry"
},
{
- "default": "0",
- "fieldname": "in_preview",
- "fieldtype": "Check",
- "label": "In Preview"
+ "default": "0",
+ "fieldname": "in_preview",
+ "fieldtype": "Check",
+ "label": "In Preview"
},
{
- "default": "0",
- "depends_on": "eval:doc.fieldtype=='Section Break'",
- "fieldname": "hide_border",
- "fieldtype": "Check",
- "label": "Hide Border"
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_seconds",
+ "fieldtype": "Check",
+ "label": "Hide Seconds"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_days",
+ "fieldtype": "Check",
+ "label": "Hide Days"
+ },
+ {
+ "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-27 11:40:48.325481",
+ "modified": "2020-02-06 23:43:00.123575",
"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",
diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py
index 21679c5bc7..a24777a80a 100644
--- a/frappe/custom/doctype/custom_field/custom_field.py
+++ b/frappe/custom/doctype/custom_field/custom_field.py
@@ -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
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index 6a54d9c7e6..d4eeba3f93 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -77,7 +77,9 @@ docfield_properties = {
'allow_bulk_edit': 'Check',
'auto_repeat': 'Link',
'allow_in_quick_entry': 'Check',
- 'hide_border': 'Check'
+ 'hide_border': 'Check',
+ 'hide_days': 'Check',
+ 'hide_seconds': 'Check'
}
allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'),
diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json
index 2c5fb874f7..267213517c 100644
--- a/frappe/custom/doctype/customize_form_field/customize_form_field.json
+++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json
@@ -11,6 +11,8 @@
"label",
"fieldtype",
"fieldname",
+ "hide_seconds",
+ "hide_days",
"reqd",
"unique",
"in_list_view",
@@ -58,350 +60,364 @@
],
"fields": [
{
- "fieldname": "label_and_type",
- "fieldtype": "Section Break",
- "label": "Label and Type"
+ "fieldname": "label_and_type",
+ "fieldtype": "Section Break",
+ "label": "Label and Type"
},
{
- "fieldname": "label",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Label",
- "oldfieldname": "label",
- "oldfieldtype": "Data",
- "search_index": 1
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Label",
+ "oldfieldname": "label",
+ "oldfieldtype": "Data",
+ "search_index": 1
},
{
- "default": "Data",
- "fieldname": "fieldtype",
- "fieldtype": "Select",
- "in_list_view": 1,
- "label": "Type",
- "oldfieldname": "fieldtype",
- "oldfieldtype": "Select",
- "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime",
- "reqd": 1,
- "search_index": 1
+ "default": "Data",
+ "fieldname": "fieldtype",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Type",
+ "oldfieldname": "fieldtype",
+ "oldfieldtype": "Select",
+ "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime",
+ "reqd": 1,
+ "search_index": 1
},
{
- "fieldname": "fieldname",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Name",
- "oldfieldname": "fieldname",
- "oldfieldtype": "Data",
- "read_only": 1,
- "search_index": 1
+ "fieldname": "fieldname",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Name",
+ "oldfieldname": "fieldname",
+ "oldfieldtype": "Data",
+ "read_only": 1,
+ "search_index": 1
},
{
- "default": "0",
- "depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
- "fieldname": "reqd",
- "fieldtype": "Check",
- "label": "Mandatory",
- "oldfieldname": "reqd",
- "oldfieldtype": "Check",
- "print_width": "50px",
- "width": "50px"
+ "default": "0",
+ "depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
+ "fieldname": "reqd",
+ "fieldtype": "Check",
+ "label": "Mandatory",
+ "oldfieldname": "reqd",
+ "oldfieldtype": "Check",
+ "print_width": "50px",
+ "width": "50px"
},
{
- "default": "0",
- "fieldname": "unique",
- "fieldtype": "Check",
- "label": "Unique"
+ "default": "0",
+ "fieldname": "unique",
+ "fieldtype": "Check",
+ "label": "Unique"
},
{
- "default": "0",
- "fieldname": "in_list_view",
- "fieldtype": "Check",
- "label": "In List View"
+ "default": "0",
+ "fieldname": "in_list_view",
+ "fieldtype": "Check",
+ "label": "In List View"
},
{
- "default": "0",
- "fieldname": "in_standard_filter",
- "fieldtype": "Check",
- "label": "In Standard Filter"
+ "default": "0",
+ "fieldname": "in_standard_filter",
+ "fieldtype": "Check",
+ "label": "In Standard Filter"
},
{
- "default": "0",
- "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
- "fieldname": "in_global_search",
- "fieldtype": "Check",
- "label": "In Global Search"
+ "default": "0",
+ "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
+ "fieldname": "in_global_search",
+ "fieldtype": "Check",
+ "label": "In Global Search"
},
{
- "default": "0",
- "fieldname": "bold",
- "fieldtype": "Check",
- "label": "Bold"
+ "default": "0",
+ "fieldname": "bold",
+ "fieldtype": "Check",
+ "label": "Bold"
},
{
- "default": "1",
- "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
- "fieldname": "translatable",
- "fieldtype": "Check",
- "label": "Translatable"
+ "default": "1",
+ "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
+ "fieldname": "translatable",
+ "fieldtype": "Check",
+ "label": "Translatable"
},
{
- "fieldname": "column_break_7",
- "fieldtype": "Column Break"
+ "fieldname": "column_break_7",
+ "fieldtype": "Column Break"
},
{
- "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
- "description": "Set non-standard precision for a Float or Currency field",
- "fieldname": "precision",
- "fieldtype": "Select",
- "label": "Precision",
- "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
+ "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
+ "description": "Set non-standard precision for a Float or Currency field",
+ "fieldname": "precision",
+ "fieldtype": "Select",
+ "label": "Precision",
+ "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
},
{
- "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'], doc.fieldtype)",
- "fieldname": "length",
- "fieldtype": "Int",
- "label": "Length"
+ "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'], doc.fieldtype)",
+ "fieldname": "length",
+ "fieldtype": "Int",
+ "label": "Length"
},
{
- "description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.",
- "fieldname": "options",
- "fieldtype": "Small Text",
- "in_list_view": 1,
- "label": "Options",
- "oldfieldname": "options",
- "oldfieldtype": "Text"
+ "description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.",
+ "fieldname": "options",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Options",
+ "oldfieldname": "options",
+ "oldfieldtype": "Text"
},
{
- "fieldname": "fetch_from",
- "fieldtype": "Small Text",
- "label": "Fetch From"
+ "fieldname": "fetch_from",
+ "fieldtype": "Small Text",
+ "label": "Fetch From"
},
{
- "default": "0",
- "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
- "fieldname": "fetch_if_empty",
- "fieldtype": "Check",
- "label": "Fetch If Empty"
+ "default": "0",
+ "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
+ "fieldname": "fetch_if_empty",
+ "fieldtype": "Check",
+ "label": "Fetch If Empty"
},
{
- "fieldname": "permissions",
- "fieldtype": "Section Break",
- "label": "Permissions"
+ "fieldname": "permissions",
+ "fieldtype": "Section Break",
+ "label": "Permissions"
},
{
- "description": "This field will appear only if the fieldname defined here has value OR the rules are true (examples): \nmyfield\neval:doc.myfield=='My Value'\neval:doc.age>18",
- "fieldname": "depends_on",
- "fieldtype": "Code",
- "label": "Depends On",
- "oldfieldname": "depends_on",
- "oldfieldtype": "Data",
- "options": "JS"
+ "description": "This field will appear only if the fieldname defined here has value OR the rules are true (examples): \nmyfield\neval:doc.myfield=='My Value'\neval:doc.age>18",
+ "fieldname": "depends_on",
+ "fieldtype": "Code",
+ "label": "Depends On",
+ "oldfieldname": "depends_on",
+ "oldfieldtype": "Data",
+ "options": "JS"
},
{
- "default": "0",
- "fieldname": "permlevel",
- "fieldtype": "Int",
- "in_list_view": 1,
- "label": "Perm Level",
- "oldfieldname": "permlevel",
- "oldfieldtype": "Int"
+ "default": "0",
+ "fieldname": "permlevel",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Perm Level",
+ "oldfieldname": "permlevel",
+ "oldfieldtype": "Int"
},
{
- "default": "0",
- "fieldname": "hidden",
- "fieldtype": "Check",
- "label": "Hidden",
- "oldfieldname": "hidden",
- "oldfieldtype": "Check",
- "print_width": "50px",
- "width": "50px"
+ "default": "0",
+ "fieldname": "hidden",
+ "fieldtype": "Check",
+ "label": "Hidden",
+ "oldfieldname": "hidden",
+ "oldfieldtype": "Check",
+ "print_width": "50px",
+ "width": "50px"
},
{
- "default": "0",
- "fieldname": "read_only",
- "fieldtype": "Check",
- "label": "Read Only"
+ "default": "0",
+ "fieldname": "read_only",
+ "fieldtype": "Check",
+ "label": "Read Only"
},
{
- "default": "0",
- "depends_on": "eval:doc.fieldtype==\"Section Break\"",
- "fieldname": "collapsible",
- "fieldtype": "Check",
- "label": "Collapsible"
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype==\"Section Break\"",
+ "fieldname": "collapsible",
+ "fieldtype": "Check",
+ "label": "Collapsible"
},
{
- "default": "0",
- "depends_on": "eval: doc.fieldtype == \"Table\"",
- "fieldname": "allow_bulk_edit",
- "fieldtype": "Check",
- "label": "Allow Bulk Edit"
+ "default": "0",
+ "depends_on": "eval: doc.fieldtype == \"Table\"",
+ "fieldname": "allow_bulk_edit",
+ "fieldtype": "Check",
+ "label": "Allow Bulk Edit"
},
{
- "depends_on": "eval:doc.fieldtype==\"Section Break\"",
- "fieldname": "collapsible_depends_on",
- "fieldtype": "Code",
- "label": "Collapsible Depends On",
- "options": "JS"
+ "depends_on": "eval:doc.fieldtype==\"Section Break\"",
+ "fieldname": "collapsible_depends_on",
+ "fieldtype": "Code",
+ "label": "Collapsible Depends On",
+ "options": "JS"
},
{
- "fieldname": "column_break_14",
- "fieldtype": "Column Break"
+ "fieldname": "column_break_14",
+ "fieldtype": "Column Break"
},
{
- "default": "0",
- "fieldname": "ignore_user_permissions",
- "fieldtype": "Check",
- "label": "Ignore User Permissions"
+ "default": "0",
+ "fieldname": "ignore_user_permissions",
+ "fieldtype": "Check",
+ "label": "Ignore User Permissions"
},
{
- "default": "0",
- "fieldname": "allow_on_submit",
- "fieldtype": "Check",
- "label": "Allow on Submit",
- "oldfieldname": "allow_on_submit",
- "oldfieldtype": "Check"
+ "default": "0",
+ "fieldname": "allow_on_submit",
+ "fieldtype": "Check",
+ "label": "Allow on Submit",
+ "oldfieldname": "allow_on_submit",
+ "oldfieldtype": "Check"
},
{
- "default": "0",
- "fieldname": "report_hide",
- "fieldtype": "Check",
- "label": "Report Hide",
- "oldfieldname": "report_hide",
- "oldfieldtype": "Check"
+ "default": "0",
+ "fieldname": "report_hide",
+ "fieldtype": "Check",
+ "label": "Report Hide",
+ "oldfieldname": "report_hide",
+ "oldfieldtype": "Check"
},
{
- "default": "0",
- "depends_on": "eval:(doc.fieldtype == 'Link')",
- "fieldname": "remember_last_selected_value",
- "fieldtype": "Check",
- "label": "Remember Last Selected Value"
+ "default": "0",
+ "depends_on": "eval:(doc.fieldtype == 'Link')",
+ "fieldname": "remember_last_selected_value",
+ "fieldtype": "Check",
+ "label": "Remember Last Selected Value"
},
{
- "fieldname": "display",
- "fieldtype": "Section Break",
- "label": "Display"
+ "fieldname": "display",
+ "fieldtype": "Section Break",
+ "label": "Display"
},
{
- "fieldname": "default",
- "fieldtype": "Text",
- "label": "Default",
- "oldfieldname": "default",
- "oldfieldtype": "Text"
+ "fieldname": "default",
+ "fieldtype": "Text",
+ "label": "Default",
+ "oldfieldname": "default",
+ "oldfieldtype": "Text"
},
{
- "default": "0",
- "fieldname": "in_filter",
- "fieldtype": "Check",
- "label": "In Filter",
- "oldfieldname": "in_filter",
- "oldfieldtype": "Check",
- "print_width": "50px",
- "width": "50px"
+ "default": "0",
+ "fieldname": "in_filter",
+ "fieldtype": "Check",
+ "label": "In Filter",
+ "oldfieldname": "in_filter",
+ "oldfieldtype": "Check",
+ "print_width": "50px",
+ "width": "50px"
},
{
- "fieldname": "column_break_21",
- "fieldtype": "Column Break"
+ "fieldname": "column_break_21",
+ "fieldtype": "Column Break"
},
{
- "fieldname": "description",
- "fieldtype": "Text",
- "label": "Description",
- "oldfieldname": "description",
- "oldfieldtype": "Text",
- "print_width": "300px",
- "width": "300px"
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "label": "Description",
+ "oldfieldname": "description",
+ "oldfieldtype": "Text",
+ "print_width": "300px",
+ "width": "300px"
},
{
- "default": "0",
- "fieldname": "print_hide",
- "fieldtype": "Check",
- "label": "Print Hide",
- "oldfieldname": "print_hide",
- "oldfieldtype": "Check"
+ "default": "0",
+ "fieldname": "print_hide",
+ "fieldtype": "Check",
+ "label": "Print Hide",
+ "oldfieldname": "print_hide",
+ "oldfieldtype": "Check"
},
{
- "default": "0",
- "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
- "fieldname": "print_hide_if_no_value",
- "fieldtype": "Check",
- "label": "Print Hide If No Value"
+ "default": "0",
+ "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
+ "fieldname": "print_hide_if_no_value",
+ "fieldtype": "Check",
+ "label": "Print Hide If No Value"
},
{
- "description": "Print Width of the field, if the field is a column in a table",
- "fieldname": "print_width",
- "fieldtype": "Data",
- "label": "Print Width",
- "print_width": "50px",
- "width": "50px"
+ "description": "Print Width of the field, if the field is a column in a table",
+ "fieldname": "print_width",
+ "fieldtype": "Data",
+ "label": "Print Width",
+ "print_width": "50px",
+ "width": "50px"
},
{
- "depends_on": "eval:cur_frm.doc.istable",
- "description": "Number of columns for a field in a Grid (Total Columns in a grid should be less than 11)",
- "fieldname": "columns",
- "fieldtype": "Int",
- "label": "Columns"
+ "depends_on": "eval:cur_frm.doc.istable",
+ "description": "Number of columns for a field in a Grid (Total Columns in a grid should be less than 11)",
+ "fieldname": "columns",
+ "fieldtype": "Int",
+ "label": "Columns"
},
{
- "fieldname": "width",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Width",
- "oldfieldname": "width",
- "oldfieldtype": "Data",
- "print_width": "50px",
- "width": "50px"
+ "fieldname": "width",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Width",
+ "oldfieldname": "width",
+ "oldfieldtype": "Data",
+ "print_width": "50px",
+ "width": "50px"
},
{
- "default": "0",
- "fieldname": "is_custom_field",
- "fieldtype": "Check",
- "hidden": 1,
- "label": "Is Custom Field",
- "read_only": 1
+ "default": "0",
+ "fieldname": "is_custom_field",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Is Custom Field",
+ "read_only": 1
},
{
- "default": "0",
- "fieldname": "allow_in_quick_entry",
- "fieldtype": "Check",
- "label": "Allow in Quick Entry"
+ "default": "0",
+ "fieldname": "allow_in_quick_entry",
+ "fieldtype": "Check",
+ "label": "Allow in Quick Entry"
},
{
- "fieldname": "property_depends_on_section",
- "fieldtype": "Section Break",
- "label": "Property Depends On"
+ "fieldname": "property_depends_on_section",
+ "fieldtype": "Section Break",
+ "label": "Property Depends On"
},
{
- "fieldname": "mandatory_depends_on",
- "fieldtype": "Code",
- "label": "Mandatory Depends On",
- "options": "JS"
+ "fieldname": "mandatory_depends_on",
+ "fieldtype": "Code",
+ "label": "Mandatory Depends On",
+ "options": "JS"
},
{
- "fieldname": "column_break_33",
- "fieldtype": "Column Break"
+ "fieldname": "column_break_33",
+ "fieldtype": "Column Break"
},
{
- "fieldname": "read_only_depends_on",
- "fieldtype": "Code",
- "label": "Read Only Depends On",
- "options": "JS"
+ "fieldname": "read_only_depends_on",
+ "fieldtype": "Code",
+ "label": "Read Only Depends On",
+ "options": "JS"
},
{
- "default": "0",
- "fieldname": "in_preview",
- "fieldtype": "Check",
- "label": "In Preview"
+ "default": "0",
+ "fieldname": "in_preview",
+ "fieldtype": "Check",
+ "label": "In Preview"
},
{
- "default": "0",
- "depends_on": "eval:doc.fieldtype=='Section Break'",
- "fieldname": "hide_border",
- "fieldtype": "Check",
- "label": "Hide Border"
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_seconds",
+ "fieldtype": "Check",
+ "label": "Hide Seconds"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_days",
+ "fieldtype": "Check",
+ "label": "Hide Days"
+ },
+ {
+ "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-27 11:39:26.389300",
+ "modified": "2020-06-02 23:45:46.810868",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",
diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py
index cd053569f0..4ec89c126d 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -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):
diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql
index bd93069a3f..af537e0612 100644
--- a/frappe/database/mariadb/framework_mariadb.sql
+++ b/frappe/database/mariadb/framework_mariadb.sql
@@ -64,6 +64,8 @@ CREATE TABLE `tabDocField` (
`length` int(11) NOT NULL DEFAULT 0,
`translatable` int(1) NOT NULL DEFAULT 0,
`hide_border` int(1) NOT NULL DEFAULT 0,
+ `hide_days` int(1) NOT NULL DEFAULT 0,
+ `hide_seconds` int(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`name`),
KEY `parent` (`parent`),
KEY `label` (`label`),
diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py
index e30ef3293f..e348916705 100644
--- a/frappe/database/postgres/database.py
+++ b/frappe/database/postgres/database.py
@@ -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):
diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql
index 76309e7347..8f77ed6230 100644
--- a/frappe/database/postgres/framework_postgres.sql
+++ b/frappe/database/postgres/framework_postgres.sql
@@ -64,6 +64,8 @@ CREATE TABLE "tabDocField" (
"length" bigint NOT NULL DEFAULT 0,
"translatable" smallint NOT NULL DEFAULT 0,
"hide_border" smallint NOT NULL DEFAULT 0,
+ "hide_days" smallint NOT NULL DEFAULT 0,
+ "hide_seconds" smallint NOT NULL DEFAULT 0,
PRIMARY KEY ("name")
) ;
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index 6ca101c3a8..956308568b 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -8,12 +8,26 @@ from json import loads, dumps
from frappe import _, DoesNotExistError, ValidationError, _dict
from frappe.boot import get_allowed_pages, get_allowed_reports
from six import string_types
+from functools import wraps
from frappe.cache_manager import (
build_domain_restriced_doctype_cache,
build_domain_restriced_page_cache,
build_table_count_cache
)
+def handle_not_exist(fn):
+ @wraps(fn)
+ def wrapper(*args, **kwargs):
+ try:
+ return fn(*args, **kwargs)
+ except DoesNotExistError:
+ if frappe.message_log:
+ frappe.message_log.pop()
+ return []
+
+ return wrapper
+
+
class Workspace:
def __init__(self, page_name):
self.page_name = page_name
@@ -157,7 +171,7 @@ class Workspace:
'user_can_dismiss': self.onboarding_doc.user_can_dismiss,
'items': self.get_onboarding_steps()
}
-
+ @handle_not_exist
def get_cards(self):
cards = self.doc.cards
if not self.doc.hide_custom:
@@ -169,8 +183,8 @@ class Workspace:
def _doctype_contains_a_record(name):
exists = self.table_counts.get(name, None)
- if exists is None:
- if not frappe.db.get_value('DocType', name, 'issingle', cache=True):
+ if not exists:
+ if not frappe.db.get_value('DocType', name, 'issingle'):
exists = frappe.db.count(name)
else:
exists = True
@@ -227,6 +241,7 @@ class Workspace:
return new_data
+ @handle_not_exist
def get_charts(self):
all_charts = []
if frappe.has_permission("Dashboard Chart", throw=False):
@@ -242,6 +257,7 @@ class Workspace:
return all_charts
+ @handle_not_exist
def get_shortcuts(self):
def _in_active_domains(item):
@@ -272,6 +288,7 @@ class Workspace:
return items
+ @handle_not_exist
def get_onboarding_steps(self):
steps = []
for doc in self.onboarding_doc.get_steps():
@@ -296,21 +313,15 @@ def get_desktop_page(page):
Returns:
dict: dictionary of cards, charts and shortcuts to be displayed on website
"""
- try:
- wspace = Workspace(page)
- wspace.build_workspace()
- return {
- 'charts': wspace.charts,
- 'shortcuts': wspace.shortcuts,
- 'cards': wspace.cards,
- 'onboarding': wspace.onboarding,
- 'allow_customization': not wspace.doc.disable_user_customization
- }
-
- except DoesNotExistError:
- if frappe.message_log:
- frappe.message_log.pop()
- return None
+ wspace = Workspace(page)
+ wspace.build_workspace()
+ return {
+ 'charts': wspace.charts,
+ 'shortcuts': wspace.shortcuts,
+ 'cards': wspace.cards,
+ 'onboarding': wspace.onboarding,
+ 'allow_customization': not wspace.doc.disable_user_customization
+ }
@frappe.whitelist()
def get_desk_sidebar_items(flatten=False):
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
index e2be095fce..a10d3d96f2 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
@@ -251,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 = $(`
@@ -268,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 = [
@@ -292,6 +305,7 @@ frappe.ui.form.on('Dashboard Chart', {
}
} else if (frm.chart_filters.length) {
fields = frm.chart_filters.filter(f => f.fieldname);
+
fields.map( f => {
if (filters[f.fieldname]) {
let condition = '=';
@@ -318,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) {
@@ -351,8 +365,15 @@ frappe.ui.form.on('Dashboard Chart', {
}
dialog.show();
- //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});
+
+ 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);
});
},
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
index 6cb8f8bfd9..ab1863ca0b 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
@@ -137,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'
@@ -150,23 +149,18 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
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": [{
@@ -261,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
@@ -358,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'))
diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
index 4425c4fd45..dfc6edbf58 100644
--- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
@@ -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()
diff --git a/frappe/desk/doctype/desk_page/desk_page.py b/frappe/desk/doctype/desk_page/desk_page.py
index dd9cc0706a..f14535cb5f 100644
--- a/frappe/desk/doctype/desk_page/desk_page.py
+++ b/frappe/desk/doctype/desk_page/desk_page.py
@@ -20,6 +20,17 @@ class DeskPage(Document):
if frappe.conf.developer_mode and self.is_standard:
export_to_files(record_list=[['Desk Page', self.name]], record_module=self.module)
+ @staticmethod
+ def get_module_page_map():
+ filters = {
+ 'extends_another_page': 0,
+ 'for_user': '',
+ }
+
+ pages = frappe.get_all("Desk Page", fields=["name", "module"], filters=filters, as_list=1)
+
+ return { page[1]: page[0] for page in pages }
+
def disable_saving_as_standard():
return frappe.flags.in_install or \
frappe.flags.in_patch or \
diff --git a/frappe/desk/doctype/event/test_event.py b/frappe/desk/doctype/event/test_event.py
index dcfb38bd08..2926a74a55 100644
--- a/frappe/desk/doctype/event/test_event.py
+++ b/frappe/desk/doctype/event/test_event.py
@@ -93,10 +93,10 @@ class TestEvent(unittest.TestCase):
self.assertEqual(set(json.loads(ev._assign)), set(["test@example.com", self.test_user]))
- # close an assignment
+ # Remove an assignment
todo = frappe.get_doc("ToDo", {"reference_type": ev.doctype, "reference_name": ev.name,
"owner": self.test_user})
- todo.status = "Closed"
+ todo.status = "Cancelled"
todo.save()
ev = frappe.get_doc("Event", ev.name)
diff --git a/frappe/desk/doctype/list_view_setting/list_view_setting.js b/frappe/desk/doctype/list_view_setting/list_view_setting.js
deleted file mode 100644
index 2c70ddf82d..0000000000
--- a/frappe/desk/doctype/list_view_setting/list_view_setting.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2019, Frappe Technologies and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('List View Setting', {
- // refresh: function(frm) {
-
- // }
-});
diff --git a/frappe/desk/doctype/list_view_setting/list_view_setting.json b/frappe/desk/doctype/list_view_setting/list_view_setting.json
deleted file mode 100644
index cd18d3f766..0000000000
--- a/frappe/desk/doctype/list_view_setting/list_view_setting.json
+++ /dev/null
@@ -1,160 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "Prompt",
- "beta": 0,
- "creation": "2019-03-06 13:29:21.101860",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "disable_count",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Disable Count",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "disable_sidebar_stats",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Disable Sidebar Stats",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "disable_auto_refresh",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Disable Auto Refresh",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-03-06 13:40:59.533586",
- "modified_by": "Administrator",
- "module": "Desk",
- "name": "List View Setting",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 0,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/frappe/desk/doctype/list_view_setting/list_view_setting.py b/frappe/desk/doctype/list_view_setting/list_view_setting.py
deleted file mode 100644
index b66dc29a43..0000000000
--- a/frappe/desk/doctype/list_view_setting/list_view_setting.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2019, 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 ListViewSetting(Document):
- pass
diff --git a/frappe/desk/doctype/list_view_setting/__init__.py b/frappe/desk/doctype/list_view_settings/__init__.py
similarity index 100%
rename from frappe/desk/doctype/list_view_setting/__init__.py
rename to frappe/desk/doctype/list_view_settings/__init__.py
diff --git a/frappe/desk/doctype/list_view_settings/list_view_settings.js b/frappe/desk/doctype/list_view_settings/list_view_settings.js
new file mode 100644
index 0000000000..db33f71675
--- /dev/null
+++ b/frappe/desk/doctype/list_view_settings/list_view_settings.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('List View Settings', {
+ // refresh: function(frm) {
+
+ // }
+});
\ No newline at end of file
diff --git a/frappe/desk/doctype/list_view_settings/list_view_settings.json b/frappe/desk/doctype/list_view_settings/list_view_settings.json
new file mode 100644
index 0000000000..44761992f1
--- /dev/null
+++ b/frappe/desk/doctype/list_view_settings/list_view_settings.json
@@ -0,0 +1,76 @@
+{
+ "actions": [],
+ "autoname": "Prompt",
+ "creation": "2019-10-23 15:00:48.392374",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "disable_count",
+ "disable_sidebar_stats",
+ "disable_auto_refresh",
+ "total_fields",
+ "fields_html",
+ "fields"
+ ],
+ "fields": [
+ {
+ "default": "0",
+ "fieldname": "disable_count",
+ "fieldtype": "Check",
+ "label": "Disable Count"
+ },
+ {
+ "default": "0",
+ "fieldname": "disable_sidebar_stats",
+ "fieldtype": "Check",
+ "label": "Disable Sidebar Stats"
+ },
+ {
+ "default": "0",
+ "fieldname": "disable_auto_refresh",
+ "fieldtype": "Check",
+ "label": "Disable Auto Refresh"
+ },
+ {
+ "fieldname": "total_fields",
+ "fieldtype": "Select",
+ "label": "Maximum Number of Fields",
+ "options": "\n4\n5\n6\n7\n8\n9\n10"
+ },
+ {
+ "fieldname": "fields_html",
+ "fieldtype": "HTML",
+ "label": "Fields"
+ },
+ {
+ "fieldname": "fields",
+ "fieldtype": "Code",
+ "hidden": 1,
+ "label": "Fields",
+ "read_only": 1
+ }
+ ],
+ "links": [],
+ "modified": "2020-05-12 18:27:15.568199",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "List View Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "read_only": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/list_view_settings/list_view_settings.py b/frappe/desk/doctype/list_view_settings/list_view_settings.py
new file mode 100644
index 0000000000..74e029f499
--- /dev/null
+++ b/frappe/desk/doctype/list_view_settings/list_view_settings.py
@@ -0,0 +1,79 @@
+# -*- 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 ListViewSettings(Document):
+
+ def on_update(self):
+ frappe.clear_document_cache(self.doctype, self.name)
+
+@frappe.whitelist()
+def save_listview_settings(doctype, listview_settings, removed_listview_fields):
+
+ listview_settings = frappe.parse_json(listview_settings)
+ removed_listview_fields = frappe.parse_json(removed_listview_fields)
+
+ if frappe.get_all("List View Settings", filters={"name": doctype}):
+ doc = frappe.get_doc("List View Settings", doctype)
+ doc.update(listview_settings)
+ doc.save()
+ else:
+ doc = frappe.new_doc("List View Settings")
+ doc.name = doctype
+ doc.update(listview_settings)
+ doc.insert()
+
+ set_listview_fields(doctype, listview_settings.get("fields"), removed_listview_fields)
+
+ return {
+ "meta": frappe.get_meta(doctype, False),
+ "listview_settings": doc
+ }
+
+def set_listview_fields(doctype, listview_fields, removed_listview_fields):
+ meta = frappe.get_meta(doctype)
+
+ listview_fields = [f.get("fieldname") for f in frappe.parse_json(listview_fields) if f.get("fieldname")]
+
+ for field in removed_listview_fields:
+ set_in_list_view_property(doctype, meta.get_field(field), "0")
+
+ for field in listview_fields:
+ set_in_list_view_property(doctype, meta.get_field(field), "1")
+
+def set_in_list_view_property(doctype, field, value):
+ if not field or field.fieldname == "status_field":
+ return
+
+ property_setter = frappe.db.get_value("Property Setter", {"doc_type": doctype, "field_name": field.fieldname, "property": "in_list_view"})
+ if property_setter:
+ doc = frappe.get_doc("Property Setter", property_setter)
+ doc.value = value
+ doc.save()
+ else:
+ frappe.make_property_setter({
+ "doctype": doctype,
+ "doctype_or_field": "DocField",
+ "fieldname": field.fieldname,
+ "property": "in_list_view",
+ "value": value,
+ "property_type": "Check"
+ }, ignore_validate=True)
+
+@frappe.whitelist()
+def get_default_listview_fields(doctype):
+ meta = frappe.get_meta(doctype)
+ path = frappe.get_module_path(frappe.scrub(meta.module), "doctype", frappe.scrub(meta.name), frappe.scrub(meta.name) + ".json")
+ doctype_json = frappe.get_file_json(path)
+
+ fields = [f.get("fieldname") for f in doctype_json.get("fields") if f.get("in_list_view")]
+
+ if meta.title_field:
+ if not meta.title_field.strip() in fields:
+ fields.append(meta.title_field.strip())
+
+ return fields
diff --git a/frappe/desk/doctype/list_view_setting/test_list_view_setting.py b/frappe/desk/doctype/list_view_settings/test_list_view_settings.py
similarity index 72%
rename from frappe/desk/doctype/list_view_setting/test_list_view_setting.py
rename to frappe/desk/doctype/list_view_settings/test_list_view_settings.py
index 143fc4cce7..c1b2f4a0da 100644
--- a/frappe/desk/doctype/list_view_setting/test_list_view_setting.py
+++ b/frappe/desk/doctype/list_view_settings/test_list_view_settings.py
@@ -3,7 +3,8 @@
# See license.txt
from __future__ import unicode_literals
+# import frappe
import unittest
-class TestListViewSetting(unittest.TestCase):
+class TestListViewSettings(unittest.TestCase):
pass
diff --git a/frappe/desk/doctype/notification_log/notification_log.js b/frappe/desk/doctype/notification_log/notification_log.js
index 654b2b2b06..1f381d115b 100644
--- a/frappe/desk/doctype/notification_log/notification_log.js
+++ b/frappe/desk/doctype/notification_log/notification_log.js
@@ -3,10 +3,43 @@
frappe.ui.form.on('Notification Log', {
refresh: function(frm) {
- let dt = frm.doc.document_type;
- let dn = frm.doc.document_name;
- frm.fields_dict.document_name.$input_wrapper
- .find('.control-value')
- .wrapInner(` `);
+ if (frm.doc.attached_file) {
+ frm.trigger('set_attachment');
+ } else {
+ frm.get_field('attachment_link').$wrapper.empty();
+ }
+ },
+
+ open_reference_document: function(frm) {
+ const dt = frm.doc.document_type;
+ const dn = frm.doc.document_name;
+ frappe.set_route('Form', dt, dn);
+ },
+
+ set_attachment: function(frm) {
+ const attachment = JSON.parse(frm.doc.attached_file);
+
+ const $wrapper = frm.get_field('attachment_link').$wrapper;
+ $wrapper.html(`
+
+ `);
+
+ $wrapper.find(".attached-file-link").click(() => {
+ const w = window.open(
+ frappe.urllib.get_full_url(`/api/method/frappe.utils.print_format.download_pdf?
+ doctype=${encodeURIComponent(attachment.doctype)}
+ &name=${encodeURIComponent(attachment.name)}
+ &format=${encodeURIComponent(attachment.print_format)}
+ &lang=${encodeURIComponent(attachment.lang)}`)
+ );
+ if (!w) {
+ frappe.msgprint(__("Please enable pop-ups"));
+ }
+ });
}
});
diff --git a/frappe/desk/doctype/notification_log/notification_log.json b/frappe/desk/doctype/notification_log/notification_log.json
index ecb746df64..050bf85ead 100644
--- a/frappe/desk/doctype/notification_log/notification_log.json
+++ b/frappe/desk/doctype/notification_log/notification_log.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2019-08-26 13:37:34.165254",
"doctype": "DocType",
"editable_grid": 1,
@@ -8,10 +9,12 @@
"for_user",
"type",
"email_content",
- "column_break_4",
"document_type",
"read",
"document_name",
+ "attached_file",
+ "attachment_link",
+ "open_reference_document",
"from_user"
],
"fields": [
@@ -20,57 +23,65 @@
"fieldtype": "Text",
"in_list_view": 1,
"label": "Subject",
- "read_only": 1
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "for_user",
"fieldtype": "Link",
+ "hidden": 1,
"label": "For User",
"options": "User",
- "read_only": 1
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "type",
"fieldtype": "Select",
+ "hidden": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Type",
- "options": "Mention\nEnergy Point\nAssignment\nShare",
- "read_only": 1,
- "search_index": 1
+ "options": "Mention\nEnergy Point\nAssignment\nShare\nAlert",
+ "search_index": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "email_content",
- "fieldtype": "Text",
- "label": "Email Content",
- "read_only": 1
- },
- {
- "fieldname": "column_break_4",
- "fieldtype": "Column Break"
+ "fieldtype": "Text Editor",
+ "label": "Message",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "document_type",
"fieldtype": "Link",
+ "hidden": 1,
"label": "Document Type",
"options": "DocType",
- "read_only": 1,
- "search_index": 1
+ "search_index": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "document_name",
"fieldtype": "Data",
- "label": "Document Name",
- "read_only": 1,
- "search_index": 1
+ "hidden": 1,
+ "label": "Document Link",
+ "search_index": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "from_user",
"fieldtype": "Link",
+ "hidden": 1,
"label": "From User",
"options": "User",
- "read_only": 1,
- "search_index": 1
+ "search_index": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "0",
@@ -78,26 +89,51 @@
"fieldtype": "Check",
"hidden": 1,
"ignore_user_permissions": 1,
- "label": "Read"
+ "label": "Read",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "open_reference_document",
+ "fieldtype": "Button",
+ "label": "Open Reference Document",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "attached_file",
+ "fieldtype": "Code",
+ "hidden": 1,
+ "label": "Attached File",
+ "options": "JSON",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "attachment_link",
+ "fieldtype": "HTML",
+ "label": "Attachment Link",
+ "show_days": 1,
+ "show_seconds": 1
}
],
+ "hide_toolbar": 1,
"in_create": 1,
- "modified": "2019-11-12 15:22:35.283678",
+ "links": [],
+ "modified": "2020-05-31 22:31:12.886950",
"modified_by": "umair@erpnext.com",
"module": "Desk",
"name": "Notification Log",
"owner": "Administrator",
"permissions": [
{
- "create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
- "share": 1,
- "write": 1
+ "share": 1
}
],
"sort_field": "modified",
diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py
index 17eb6371b1..211b3ae5e6 100644
--- a/frappe/desk/doctype/notification_log/notification_log.py
+++ b/frappe/desk/doctype/notification_log/notification_log.py
@@ -48,6 +48,7 @@ def enqueue_create_notification(users, doc):
if isinstance(users, frappe.string_types):
users = [user.strip() for user in users.split(',') if user.strip()]
+ users = list(set(users))
frappe.enqueue(
'frappe.desk.doctype.notification_log.notification_log.make_notification_logs',
@@ -58,6 +59,7 @@ def enqueue_create_notification(users, doc):
def make_notification_logs(doc, users):
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled
+
for user in users:
if frappe.db.exists('User', user):
if is_notifications_enabled(user):
@@ -68,7 +70,7 @@ def make_notification_logs(doc, users):
_doc.update(doc)
_doc.for_user = user
_doc.subject = _doc.subject.replace('', '').replace('
', '')
- if _doc.for_user != _doc.from_user or doc.type == 'Energy Point':
+ if _doc.for_user != _doc.from_user or doc.type == 'Energy Point' or doc.type == 'Alert':
_doc.insert(ignore_permissions=True)
def send_notification_email(doc):
diff --git a/frappe/desk/doctype/notification_settings/notification_settings.json b/frappe/desk/doctype/notification_settings/notification_settings.json
index 6af325507b..85f93e156e 100644
--- a/frappe/desk/doctype/notification_settings/notification_settings.json
+++ b/frappe/desk/doctype/notification_settings/notification_settings.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "Prompt",
"creation": "2019-09-11 22:15:44.851526",
"doctype": "DocType",
@@ -21,52 +22,68 @@
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
- "label": "Enabled"
+ "label": "Enabled",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "subscribed_documents",
"fieldtype": "Table MultiSelect",
"label": "Subscribed Documents",
- "options": "Notification Subscribed Document"
+ "options": "Notification Subscribed Document",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Section Break",
- "label": "Email Settings"
+ "label": "Email Settings",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "1",
"fieldname": "enable_email_notifications",
"fieldtype": "Check",
- "label": "Enable Email Notifications"
+ "label": "Enable Email Notifications",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "1",
"depends_on": "enable_email_notifications",
"fieldname": "enable_email_mention",
"fieldtype": "Check",
- "label": "Mentions"
+ "label": "Mentions",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "1",
"depends_on": "enable_email_notifications",
"fieldname": "enable_email_assignment",
"fieldtype": "Check",
- "label": "Assignments"
+ "label": "Assignments",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "1",
"depends_on": "enable_email_notifications",
"fieldname": "enable_email_energy_point",
"fieldtype": "Check",
- "label": "Energy Points"
+ "label": "Energy Points",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "1",
"depends_on": "enable_email_notifications",
"fieldname": "enable_email_share",
"fieldtype": "Check",
- "label": "Document Share"
+ "label": "Document Share",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "__user",
@@ -75,18 +92,23 @@
"hidden": 1,
"label": "User",
"options": "User",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "0",
"fieldname": "seen",
"fieldtype": "Check",
"hidden": 1,
- "label": "Seen"
+ "label": "Seen",
+ "show_days": 1,
+ "show_seconds": 1
}
],
"in_create": 1,
- "modified": "2019-11-19 12:57:59.356786",
+ "links": [],
+ "modified": "2020-05-31 22:16:40.798019",
"modified_by": "Administrator",
"module": "Desk",
"name": "Notification Settings",
diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py
index 6b5a13ee27..9b124cd6f4 100644
--- a/frappe/desk/doctype/notification_settings/notification_settings.py
+++ b/frappe/desk/doctype/notification_settings/notification_settings.py
@@ -28,6 +28,9 @@ def is_email_notifications_enabled_for_type(user, notification_type):
if not is_email_notifications_enabled(user):
return False
+ if notification_type == 'Alert':
+ return False
+
fieldname = 'enable_email_' + frappe.scrub(notification_type)
enabled = frappe.db.get_value('Notification Settings', user, fieldname)
if enabled is None:
diff --git a/frappe/desk/doctype/todo/todo.py b/frappe/desk/doctype/todo/todo.py
index 8e8102d093..804174b56b 100644
--- a/frappe/desk/doctype/todo/todo.py
+++ b/frappe/desk/doctype/todo/todo.py
@@ -64,7 +64,7 @@ class ToDo(Document):
filters={
"reference_type": self.reference_type,
"reference_name": self.reference_name,
- "status": "Open"
+ "status": ("!=", "Cancelled")
},
fields=["owner"], as_list=True)]
diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py
index 6c679bf312..72917d0341 100644
--- a/frappe/desk/form/linked_with.py
+++ b/frappe/desk/form/linked_with.py
@@ -13,7 +13,7 @@ from frappe.modules import load_doctype_module
@frappe.whitelist()
-def get_submitted_linked_docs(doctype, name, docs=None):
+def get_submitted_linked_docs(doctype, name, docs=None, linked=None):
"""
Get all nested submitted linked doctype linkinfo
@@ -31,12 +31,26 @@ def get_submitted_linked_docs(doctype, name, docs=None):
if not docs:
docs = []
+ if not linked:
+ linked = {}
+
linkinfo = get_linked_doctypes(doctype)
linked_docs = get_linked_docs(doctype, name, linkinfo)
link_count = 0
for link_doctype, link_names in linked_docs.items():
+ if link_doctype not in linked:
+ linked[link_doctype] = []
+
for link in link_names:
+ if link['name'] == name:
+ continue
+
+ if linked and name in linked[link_doctype]:
+ continue
+
+ linked[link_doctype].append(link['name'])
+
docinfo = link.update({"doctype": link_doctype})
validated_doc = validate_linked_doc(docinfo)
@@ -47,7 +61,7 @@ def get_submitted_linked_docs(doctype, name, docs=None):
if link.name in [doc.get("name") for doc in docs]:
continue
- links = get_submitted_linked_docs(link_doctype, link.name, docs)
+ links = get_submitted_linked_docs(link_doctype, link.name, docs, linked)
docs.append({
"doctype": link_doctype,
"name": link.name,
diff --git a/frappe/desk/listview.py b/frappe/desk/listview.py
index e7f56d313e..1bce14fb2d 100644
--- a/frappe/desk/listview.py
+++ b/frappe/desk/listview.py
@@ -7,17 +7,16 @@ import frappe
@frappe.whitelist()
def get_list_settings(doctype):
try:
- return frappe.get_cached_doc("List View Setting", doctype)
+ return frappe.get_cached_doc("List View Settings", doctype)
except frappe.DoesNotExistError:
frappe.clear_messages()
-
@frappe.whitelist()
def set_list_settings(doctype, values):
try:
- doc = frappe.get_doc("List View Setting", doctype)
+ doc = frappe.get_doc("List View Settings", doctype)
except frappe.DoesNotExistError:
- doc = frappe.new_doc("List View Setting")
+ doc = frappe.new_doc("List View Settings")
doc.name = doctype
frappe.clear_messages()
doc.update(frappe.parse_json(values))
diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py
index 4a1302788b..4b584a2429 100644
--- a/frappe/desk/notifications.py
+++ b/frappe/desk/notifications.py
@@ -252,7 +252,7 @@ def get_open_count(doctype, name, items=[]):
continue
filters = get_filters_for(d)
- fieldname = links.get("non_standard_fieldnames", {}).get(d, links.fieldname)
+ fieldname = links.get("non_standard_fieldnames", {}).get(d, links.get('fieldname'))
data = {"name": d}
if filters:
# get the fieldname for the current document
diff --git a/frappe/desk/page/user_profile/user_profile.js b/frappe/desk/page/user_profile/user_profile.js
index c43ff27ba3..646c31f7a1 100644
--- a/frappe/desk/page/user_profile/user_profile.js
+++ b/frappe/desk/page/user_profile/user_profile.js
@@ -110,7 +110,11 @@ class UserProfile {
render_line_chart() {
- this.line_chart_filters = [['Energy Point Log', 'user', '=', this.user_id, false]];
+ this.line_chart_filters = [
+ ['Energy Point Log', 'user', '=', this.user_id, false],
+ ['Energy Point Log', 'type', '!=', 'Review', false]
+ ];
+
this.line_chart_config = {
timespan: 'Last Month',
time_interval: 'Daily',
@@ -186,7 +190,10 @@ class UserProfile {
options: ['All', 'Auto', 'Criticism', 'Appreciation', 'Revert'],
action: (selected_item) => {
if (selected_item === 'All') {
- if (this.line_chart_filters.length > 1) this.line_chart_filters.pop();
+ this.line_chart_filters = [
+ ['Energy Point Log', 'user', '=', this.user_id, false],
+ ['Energy Point Log', 'type', '!=', 'Review', false]
+ ];
} else {
this.line_chart_filters[1] = ['Energy Point Log', 'type', '=', selected_item, false];
}
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index 74e841f107..0edfd57d4f 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -62,8 +62,16 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
ljust_list(res, 6)
if report.custom_columns:
+ # Original query columns, needed to reorder data as per custom columns
+ query_columns = columns
+ # Reordered columns
columns = json.loads(report.custom_columns)
+
+ if report.report_type == 'Query Report':
+ result = reorder_data_for_custom_columns(columns, query_columns, result)
+
result = add_data_to_custom_columns(columns, result)
+
if custom_columns:
result = add_data_to_custom_columns(custom_columns, result)
@@ -208,6 +216,23 @@ def add_data_to_custom_columns(columns, result):
return data
+def reorder_data_for_custom_columns(custom_columns, columns, result):
+ reordered_result = []
+ columns = [col.split(":")[0] for col in columns]
+
+ for res in result:
+ r = []
+ for col in custom_columns:
+ try:
+ idx = columns.index(col.get("label"))
+ r.append(res[idx])
+ except ValueError:
+ pass
+
+ reordered_result.append(r)
+
+ return reordered_result
+
def get_prepared_report_result(report, filters, dn="", user=None):
latest_report_data = {}
doc = None
diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py
index cb00614019..539f6c9db8 100644
--- a/frappe/email/doctype/auto_email_report/auto_email_report.py
+++ b/frappe/email/doctype/auto_email_report/auto_email_report.py
@@ -216,8 +216,10 @@ def send_daily():
elif auto_email_report.frequency == 'Weekly':
if auto_email_report.day_of_week != current_day:
continue
-
- auto_email_report.send()
+ try:
+ auto_email_report.send()
+ except Exception as e:
+ frappe.log_error(e, _('Failed to send {0} Auto Email Report').format(auto_email_report.name))
def send_monthly():
diff --git a/frappe/email/doctype/email_account/email_account.json b/frappe/email/doctype/email_account/email_account.json
index 6bde0291a0..057638697a 100644
--- a/frappe/email/doctype/email_account/email_account.json
+++ b/frappe/email/doctype/email_account/email_account.json
@@ -29,6 +29,7 @@
"default_incoming",
"email_sync_option",
"initial_sync_count",
+ "create_contact",
"section_break_12",
"enable_automatic_linking",
"section_break_13",
@@ -114,9 +115,9 @@
"depends_on": "eval:!doc.service",
"fieldname": "domain",
"fieldtype": "Link",
- "label": "Domain",
"in_list_view": 1,
"in_standard_filter": 1,
+ "label": "Domain",
"options": "Email Domain"
},
{
@@ -408,11 +409,17 @@
"fieldname": "use_ssl_for_outgoing",
"fieldtype": "Check",
"label": "Use SSL for Outgoing"
+ },
+ {
+ "default": "1",
+ "fieldname": "create_contact",
+ "fieldtype": "Check",
+ "label": "Create Contacts from Incoming Emails"
}
],
"icon": "fa fa-inbox",
"links": [],
- "modified": "2020-04-06 19:20:50.491146",
+ "modified": "2020-05-11 15:18:43.931499",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Account",
@@ -427,11 +434,11 @@
"write": 1
},
{
- "read": 1,
- "role": "Inbox User"
+ "read": 1,
+ "role": "Inbox User"
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/email/doctype/notification/notification.js b/frappe/email/doctype/notification/notification.js
index 44056955f7..02fc8512ca 100644
--- a/frappe/email/doctype/notification/notification.js
+++ b/frappe/email/doctype/notification/notification.js
@@ -80,7 +80,6 @@ frappe.ui.form.on("Notification", {
});
},
refresh: function(frm) {
- frm.toggle_reqd("recipients", frm.doc.channel=="Email");
frappe.notification.setup_fieldname_select(frm);
frm.get_field("is_standard").toggle(frappe.boot.developer_mode);
frm.trigger('event');
diff --git a/frappe/email/doctype/notification/notification.json b/frappe/email/doctype/notification/notification.json
index 14eff2251a..d1526f5fe4 100644
--- a/frappe/email/doctype/notification/notification.json
+++ b/frappe/email/doctype/notification/notification.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2014-07-11 17:18:09.923399",
@@ -22,6 +23,7 @@
"days_in_advance",
"value_changed",
"sender",
+ "send_system_notification",
"sender_email",
"section_break_9",
"condition",
@@ -46,32 +48,43 @@
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
- "label": "Enabled"
+ "label": "Enabled",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_2",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "Email",
+ "depends_on": "eval: !doc.disable_channel",
"fieldname": "channel",
"fieldtype": "Select",
"label": "Channel",
- "options": "Email\nSlack",
+ "options": "Email\nSlack\nSystem Notification",
"reqd": 1,
- "set_only_once": 1
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "eval:doc.channel=='Slack'",
"fieldname": "slack_webhook_url",
"fieldtype": "Link",
"label": "Slack Channel",
- "options": "Slack Webhook URL"
+ "mandatory_depends_on": "eval:doc.channel=='Slack'",
+ "options": "Slack Webhook URL",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "filters",
"fieldtype": "Section Break",
- "label": "Filters"
+ "label": "Filters",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"description": "To add dynamic subject, use jinja tags like\n\n",
@@ -80,7 +93,9 @@
"ignore_xss_filter": 1,
"in_list_view": 1,
"label": "Subject",
- "reqd": 1
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "document_type",
@@ -90,13 +105,17 @@
"label": "Document Type",
"options": "DocType",
"reqd": 1,
- "search_index": 1
+ "search_index": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "0",
"fieldname": "is_standard",
"fieldtype": "Check",
- "label": "Is Standard"
+ "label": "Is Standard",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "is_standard",
@@ -104,11 +123,15 @@
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Module",
- "options": "Module Def"
+ "options": "Module Def",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "col_break_1",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "event",
@@ -117,21 +140,27 @@
"label": "Send Alert On",
"options": "\nNew\nSave\nSubmit\nCancel\nDays After\nDays Before\nValue Change\nMethod\nCustom",
"reqd": 1,
- "search_index": 1
+ "search_index": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "eval:doc.event=='Method'",
"description": "Trigger on valid methods like \"before_insert\", \"after_update\", etc (will depend on the DocType selected)",
"fieldname": "method",
"fieldtype": "Data",
- "label": "Trigger Method"
+ "label": "Trigger Method",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "eval:doc.event==\"Days After\" || doc.event==\"Days Before\"",
"description": "Send alert if date matches this field's value",
"fieldname": "date_changed",
"fieldtype": "Select",
- "label": "Reference Date"
+ "label": "Reference Date",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "0",
@@ -139,31 +168,41 @@
"description": "Send days before or after the reference date",
"fieldname": "days_in_advance",
"fieldtype": "Int",
- "label": "Days Before or After"
+ "label": "Days Before or After",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "eval:doc.event==\"Value Change\"",
"description": "Send alert if this field's value changes",
"fieldname": "value_changed",
"fieldtype": "Select",
- "label": "Value Changed"
+ "label": "Value Changed",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "sender",
"fieldtype": "Link",
"label": "Sender",
- "options": "Email Account"
+ "options": "Email Account",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "sender_email",
"fieldtype": "Data",
"label": "Sender Email",
"options": "Email",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "section_break_9",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"description": "Optional: The alert will be sent if this expression is true",
@@ -171,99 +210,143 @@
"fieldtype": "Code",
"ignore_xss_filter": 1,
"in_list_view": 1,
- "label": "Condition"
+ "label": "Condition",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_6",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "html_7",
"fieldtype": "HTML",
- "options": "Condition Examples:
\ndoc.status==\"Open\" doc.due_date==nowdate() doc.total > 40000\n \n"
+ "options": "Condition Examples:
\ndoc.status==\"Open\" doc.due_date==nowdate() doc.total > 40000\n \n",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "property_section",
"fieldtype": "Section Break",
- "label": "Set Property After Alert"
+ "label": "Set Property After Alert",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "set_property_after_alert",
"fieldtype": "Select",
- "label": "Set Property After Alert"
+ "label": "Set Property After Alert",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "property_value",
"fieldtype": "Data",
- "label": "Value To Be Set"
+ "label": "Value To Be Set",
+ "show_days": 1,
+ "show_seconds": 1
},
{
- "depends_on": "eval:doc.channel=='Email'",
+ "depends_on": "eval:doc.channel!=='Slack'",
"fieldname": "column_break_5",
"fieldtype": "Section Break",
- "label": "Recipients"
+ "label": "Recipients",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "recipients",
"fieldtype": "Table",
"label": "Recipients",
- "options": "Notification Recipient"
+ "mandatory_depends_on": "eval:doc.channel!=='Slack'",
+ "options": "Notification Recipient",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "message_sb",
"fieldtype": "Section Break",
- "label": "Message"
+ "label": "Message",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "Add your message here",
"fieldname": "message",
"fieldtype": "Code",
"ignore_xss_filter": 1,
- "label": "Message"
+ "label": "Message",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "eval:doc.channel=='Email'",
"fieldname": "message_examples",
"fieldtype": "HTML",
"label": "Message Examples",
- "options": "Message Example \n\n<h3>Order Overdue</h3>\n\n<p>Transaction {{ doc.name }} has exceeded Due Date. Please take necessary action.</p>\n\n<!-- show last comment -->\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n<h4>Details</h4>\n\n<ul>\n<li>Customer: {{ doc.customer }}\n<li>Amount: {{ doc.grand_total }}\n</ul>\n "
+ "options": "Message Example \n\n<h3>Order Overdue</h3>\n\n<p>Transaction {{ doc.name }} has exceeded Due Date. Please take necessary action.</p>\n\n<!-- show last comment -->\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n<h4>Details</h4>\n\n<ul>\n<li>Customer: {{ doc.customer }}\n<li>Amount: {{ doc.grand_total }}\n</ul>\n ",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "eval:doc.channel=='Slack'",
"fieldname": "slack_message_examples",
"fieldtype": "HTML",
"label": "Message Examples",
- "options": "Message Example \n\n*Order Overdue*\n\nTransaction {{ doc.name }} has exceeded Due Date. Please take necessary action.\n\n\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n*Details*\n\n\u2022 Customer: {{ doc.customer }}\n\u2022 Amount: {{ doc.grand_total }}\n "
+ "options": "Message Example \n\n*Order Overdue*\n\nTransaction {{ doc.name }} has exceeded Due Date. Please take necessary action.\n\n\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n*Details*\n\n\u2022 Customer: {{ doc.customer }}\n\u2022 Amount: {{ doc.grand_total }}\n ",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "view_properties",
"fieldtype": "Button",
- "label": "View Properties (via Customize Form)"
+ "label": "View Properties (via Customize Form)",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
"collapsible_depends_on": "attach_print",
"fieldname": "column_break_25",
"fieldtype": "Section Break",
- "label": "Print Settings"
+ "label": "Print Settings",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "0",
"fieldname": "attach_print",
"fieldtype": "Check",
- "label": "Attach Print"
+ "label": "Attach Print",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "attach_print",
"fieldname": "print_format",
"fieldtype": "Link",
"label": "Print Format",
- "options": "Print Format"
+ "options": "Print Format",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: doc.channel !== 'System Notification'",
+ "description": "If enabled, the notification will show up in the notifications dropdown on the top right corner of the navigation bar.",
+ "fieldname": "send_system_notification",
+ "fieldtype": "Check",
+ "label": "Send System Notification",
+ "show_days": 1,
+ "show_seconds": 1
}
],
"icon": "fa fa-envelope",
- "modified": "2019-07-15 13:17:02.585013",
+ "links": [],
+ "modified": "2020-05-29 16:03:10.914526",
"modified_by": "Administrator",
"module": "Email",
"name": "Notification",
diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py
index 8c011ade65..8e53b50fa2 100644
--- a/frappe/email/doctype/notification/notification.py
+++ b/frappe/email/doctype/notification/notification.py
@@ -13,6 +13,7 @@ from frappe.utils.jinja import validate_template
from frappe.modules.utils import export_module_json, get_doc_module
from six import string_types
from frappe.integrations.doctype.slack_webhook_url.slack_webhook_url import send_slack_message
+from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification
class Notification(Document):
def onload(self):
@@ -125,6 +126,9 @@ def get_context(context):
if self.channel == 'Slack':
self.send_a_slack_msg(doc, context)
+ if self.channel == 'System Notification' or self.send_system_notification:
+ self.create_system_notification(doc, context)
+
if self.set_property_after_alert:
allow_update = True
if doc.docstatus == 1 and not doc.meta.get_field(self.set_property_after_alert).allow_on_submit:
@@ -143,6 +147,25 @@ def get_context(context):
except Exception:
frappe.log_error(title='Document update failed', message=frappe.get_traceback())
+ def create_system_notification(self, doc, context):
+ subject = self.subject
+ if "{" in subject:
+ subject = frappe.render_template(self.subject, context)
+
+ attachments = self.get_attachment(doc)
+ recipients, cc, bcc = self.get_list_of_recipients(doc, context)
+ users = recipients + cc + bcc
+
+ notification_doc = {
+ 'type': 'Alert',
+ 'document_type': doc.doctype,
+ 'document_name': doc.name,
+ 'subject': subject,
+ 'email_content': frappe.render_template(self.message, context),
+ 'attached_file': attachments and json.dumps(attachments[0])
+ }
+ enqueue_create_notification(users, notification_doc)
+
def send_an_email(self, doc, context):
from email.utils import formataddr
subject = self.subject
@@ -228,8 +251,7 @@ def get_context(context):
# ignoring attachment as draft and cancelled documents are not allowed to print
status = "Draft" if doc.docstatus == 0 else "Cancelled"
- frappe.throw(_("""Not allowed to attach {0} document,
- please enable Allow Print For {0} in Print Settings""".format(status)),
+ frappe.throw(_("""Not allowed to attach {0} document, please enable Allow Print For {0} in Print Settings""").format(status),
title=_("Error in Notification"))
else:
return [{
diff --git a/frappe/exceptions.py b/frappe/exceptions.py
index 5a1181f31e..8ebda9c7b8 100644
--- a/frappe/exceptions.py
+++ b/frappe/exceptions.py
@@ -13,6 +13,11 @@ if sys.version_info.major == 2:
else:
from builtins import FileNotFoundError
+class SiteNotSpecifiedError(Exception):
+ def __init__(self, *args, **kwargs):
+ self.message = "Please specify --site sitename"
+ super(Exception, self).__init__(self.message)
+
class ValidationError(Exception):
http_status_code = 417
@@ -98,6 +103,7 @@ class InvalidColumnName(ValidationError): pass
class IncompatibleApp(ValidationError): pass
class InvalidDates(ValidationError): pass
class DataTooLongException(ValidationError): pass
+class FileAlreadyAttachedException(Exception): pass
# OAuth exceptions
class InvalidAuthorizationHeader(CSRFTokenError): pass
class InvalidAuthorizationPrefix(CSRFTokenError): pass
diff --git a/frappe/hooks.py b/frappe/hooks.py
index 200280f6de..f5a8701089 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -56,6 +56,8 @@ website_route_rules = [
{"from_route": "/profile", "to_route": "me"},
]
+base_template = "templates/base.html"
+
write_file_keys = ["file_url", "file_name"]
notification_config = "frappe.core.notifications.get_notification_config"
@@ -270,7 +272,10 @@ setup_wizard_exception = [
]
before_migrate = ['frappe.patches.v11_0.sync_user_permission_doctype_before_migrate.execute']
-after_migrate = ['frappe.website.doctype.website_theme.website_theme.generate_theme_files_if_not_exist']
+after_migrate = [
+ 'frappe.website.doctype.website_theme.website_theme.generate_theme_files_if_not_exist',
+ 'frappe.modules.full_text_search.build_index_for_all_routes'
+]
otp_methods = ['OTP App','Email','SMS']
user_privacy_documents = [
diff --git a/frappe/installer.py b/frappe/installer.py
index 54402f0087..4fc19b282a 100755
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -269,6 +269,7 @@ def make_site_dirs():
os.path.join(site_private_path, 'backups'),
os.path.join(site_public_path, 'files'),
os.path.join(site_private_path, 'files'),
+ os.path.join(frappe.local.site_path, 'logs'),
os.path.join(frappe.local.site_path, 'task-logs')):
if not os.path.exists(dir_path):
os.makedirs(dir_path)
@@ -298,7 +299,8 @@ def remove_missing_apps():
def extract_sql_gzip(sql_gz_path):
try:
- subprocess.check_call(['gzip', '-d', '-v', '-f', sql_gz_path])
+ # kdvf - keep, decompress, verbose, force
+ subprocess.check_call(['gzip', '-kdvf', sql_gz_path])
except:
raise
diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
index 2a036f4838..f177aa6620 100644
--- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
+++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
@@ -56,7 +56,8 @@ def take_backup_to_dropbox(retry_count=0, upload_db_backup=True):
did_not_upload, error_log = backup_to_dropbox(upload_db_backup)
if did_not_upload: raise Exception
- send_email(True, "Dropbox", "Dropbox Settings", "send_notifications_to")
+ if cint(frappe.db.get_value("Dropbox Settings", None, "send_email_for_successful_backup")):
+ send_email(True, "Dropbox", "Dropbox Settings", "send_notifications_to")
except JobTimeoutException:
if retry_count < 2:
args = {
@@ -90,7 +91,7 @@ def backup_to_dropbox(upload_db_backup=True):
dropbox_settings['access_token'] = access_token['oauth2_token']
set_dropbox_access_token(access_token['oauth2_token'])
- dropbox_client = dropbox.Dropbox(dropbox_settings['access_token'])
+ dropbox_client = dropbox.Dropbox(dropbox_settings['access_token'], timeout=None)
if upload_db_backup:
if frappe.flags.create_new_backup:
diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py
index 5874c79108..6455623281 100644
--- a/frappe/integrations/doctype/google_contacts/google_contacts.py
+++ b/frappe/integrations/doctype/google_contacts/google_contacts.py
@@ -147,11 +147,14 @@ def sync_contacts_from_google_contacts(g_contact):
results = []
contacts_updated = 0
+ sync_token = account.get_password(fieldname="next_sync_token", raise_exception=False) or None
+ contacts = frappe._dict()
+
while True:
try:
- sync_token = account.get_password(fieldname="next_sync_token", raise_exception=False) or None
- contacts = google_contacts.people().connections().list(resourceName='people/me',syncToken=sync_token,
- personFields="names,emailAddresses,organizations,phoneNumbers").execute()
+ contacts = google_contacts.people().connections().list(resourceName='people/me', pageToken=contacts.get("nextPageToken"),
+ syncToken=sync_token, pageSize=2000, requestSyncToken=True, personFields="names,emailAddresses,organizations,phoneNumbers").execute()
+
except HttpError as err:
frappe.throw(_("Google Contacts - Could not sync contacts from Google Contacts {0}, error code {1}.").format(account.name, err.resp.status))
diff --git a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py
index 5e464d4882..1d2f7f9495 100644
--- a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py
+++ b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py
@@ -64,6 +64,8 @@ from __future__ import unicode_literals
import frappe
from frappe import _
import json
+import hmac
+import hashlib
from six.moves.urllib.parse import urlencode
from frappe.model.document import Document
from frappe.utils import get_url, call_hook_method, cint, get_timestamp
@@ -317,6 +319,20 @@ class RazorpaySettings(Document):
except Exception:
frappe.log_error(frappe.get_traceback())
+ def verify_signature(self, body, signature, key):
+ key = bytes(key, 'utf-8')
+ body = bytes(body, 'utf-8')
+
+ dig = hmac.new(key=key, msg=body, digestmod=hashlib.sha256)
+
+ generated_signature = dig.hexdigest()
+ result = hmac.compare_digest(generated_signature, signature)
+
+ if not result:
+ frappe.throw(_('Razorpay Signature Verification Failed'), exc=frappe.PermissionError)
+
+ return result
+
def capture_payment(is_sandbox=False, sanbox_response=None):
"""
Verifies the purchase as complete by the merchant.
diff --git a/frappe/integrations/frappe_providers/__init__.py b/frappe/integrations/frappe_providers/__init__.py
index 0b689478d2..887e191e16 100644
--- a/frappe/integrations/frappe_providers/__init__.py
+++ b/frappe/integrations/frappe_providers/__init__.py
@@ -7,7 +7,6 @@ from frappe.integrations.frappe_providers.frappecloud import frappecloud_migrato
def migrate_to(local_site, frappe_provider):
if frappe_provider in ("frappe.cloud", "frappecloud.com"):
- frappe_provider = "frappecloud.com"
return frappecloud_migrator(local_site, frappe_provider)
else:
print("{} is not supported yet".format(frappe_provider))
diff --git a/frappe/integrations/frappe_providers/frappecloud.py b/frappe/integrations/frappe_providers/frappecloud.py
index 4f33c990f9..3e4b584246 100644
--- a/frappe/integrations/frappe_providers/frappecloud.py
+++ b/frappe/integrations/frappe_providers/frappecloud.py
@@ -13,7 +13,129 @@ import requests
import frappe
import frappe.utils.backups
from frappe.utils import get_installed_apps_info
-from frappe.utils.commands import render_table, add_line_after
+from frappe.utils.commands import render_table, add_line_after, add_line_before
+
+
+# TODO: check upgrade compatibility
+
+
+def render_actions_table():
+ actions_table = [["#", "Action"]]
+ actions = []
+
+ for n, action in enumerate(migrator_actions):
+ actions_table.append([n+1, action["title"]])
+ actions.append(action["fn"])
+
+ render_table(actions_table)
+ return actions
+
+
+def render_site_table(sites_info):
+ sites_table = [["#", "Site Name", "Status"]]
+ available_sites = []
+
+ for n, site_data in enumerate(sites_info):
+ name, status = site_data["name"], site_data["status"]
+ if status in ("Active", "Broken"):
+ sites_table.append([n + 1, name, status])
+ available_sites.append(name)
+
+ render_table(sites_table)
+ return available_sites
+
+
+def render_teams_table(teams):
+ teams_table = [["#", "Team"]]
+
+ for n, team in enumerate(teams):
+ teams_table.append([n+1, team])
+
+ render_table(teams_table)
+
+
+def render_plan_table(plans_list):
+ plans_table = [["Plan", "CPU Time"]]
+ visible_headers = ["name", "cpu_time_per_day"]
+
+ for plan in plans_list:
+ plan, cpu_time = [plan[header] for header in visible_headers]
+ plans_table.append([plan, "{} hour{}/day".format(cpu_time, "" if cpu_time < 2 else "s")])
+
+ render_table(plans_table)
+
+
+def render_group_table(app_groups):
+ # title row
+ app_groups_table = [["#", "App Group", "Apps"]]
+
+ # all rows
+ for idx, app_group in enumerate(app_groups):
+ apps_list = ", ".join(["{}:{}".format(app["scrubbed"], app["branch"]) for app in app_group["apps"]])
+ row = [idx + 1, app_group["name"], apps_list]
+ app_groups_table.append(row)
+
+ render_table(app_groups_table)
+
+
+def handle_request_failure(request=None, message=None, traceback=True, exit_code=1):
+ message = message or "Request failed with error code {}".format(request.status_code)
+ response = html2text(request.text) if traceback else ""
+
+ print("{0}{1}".format(message, "\n" + response))
+ sys.exit(exit_code)
+
+
+@add_line_after
+def select_primary_action():
+ actions = render_actions_table()
+ idx = click.prompt("What do you want to do?", type=click.IntRange(1, len(actions))) - 1
+
+ return actions[idx]
+
+
+@add_line_after
+def select_site():
+ get_all_sites_request = session.post(all_site_url, headers={
+ "accept": "application/json",
+ "accept-encoding": "gzip, deflate, br",
+ "content-type": "application/json; charset=utf-8"
+ })
+
+ if get_all_sites_request.ok:
+ all_sites = get_all_sites_request.json()["message"]
+ available_sites = render_site_table(all_sites)
+
+ while True:
+ selected_site = click.prompt("Name of the site you want to restore to", type=str).strip()
+ if selected_site in available_sites:
+ return selected_site
+ else:
+ print("Site {} does not exist. Try again ❌".format(selected_site))
+ else:
+ print("Couldn't retrive sites list...Try again later")
+ sys.exit(1)
+
+
+@add_line_before
+def select_team(session):
+ # get team options
+ account_details_sc = session.post(account_details_url)
+ if account_details_sc.ok:
+ account_details = account_details_sc.json()["message"]
+ available_teams = account_details["teams"]
+
+ # ask if they want to select, go ahead with if only one exists
+ if len(available_teams) == 1:
+ team = available_teams[0]
+ else:
+ render_teams_table(available_teams)
+ idx = click.prompt("Select Team", type=click.IntRange(1, len(available_teams))) - 1
+ team = available_teams[idx]
+
+ print("Team '{}' set for current session".format(team))
+
+ return team
def get_new_site_options():
@@ -46,21 +168,6 @@ def is_subdomain_available(subdomain):
return available
-def render_plan_table(plans_list):
- plans_table = []
-
- # title row
- visible_headers = ["name", "cpu_time_per_day"]
- plans_table.append(["Plan", "CPU Time"])
-
- # all rows
- for plan in plans_list:
- plan, cpu_time = [plan[header] for header in visible_headers]
- plans_table.append([plan, "{} hour{}/day".format(cpu_time, "" if cpu_time < 2 else "s")])
-
- render_table(plans_table)
-
-
@add_line_after
def choose_plan(plans_list):
print("{} plans available".format(len(plans_list)))
@@ -113,19 +220,6 @@ def check_app_compat(available_group):
return is_compat, filtered_apps
-def render_group_table(app_groups):
- # title row
- app_groups_table = [["#", "App Group", "Apps"]]
-
- # all rows
- for idx, app_group in enumerate(app_groups):
- apps_list = ", ".join(["{}:{}".format(app["scrubbed"], app["branch"]) for app in app_group["apps"]])
- row = [idx + 1, app_group["name"], apps_list]
- app_groups_table.append(row)
-
- render_table(app_groups_table)
-
-
@add_line_after
def filter_apps(app_groups):
render_group_table(app_groups)
@@ -148,24 +242,6 @@ def filter_apps(app_groups):
return selected_group["name"], filtered_apps
-@add_line_after
-def create_session():
- # take user input from STDIN
- username = click.prompt("Username").strip()
- password = getpass.unix_getpass()
-
- auth_credentials = {"usr": username, "pwd": password}
-
- session = requests.Session()
- login_sc = session.post(login_url, auth_credentials)
-
- if login_sc.ok:
- print("Authorization Successful! ✅")
- session.headers.update({"X-Press-Team": username})
- return session
- else:
- print("Authorization Failed with Error Code {}".format(login_sc.status_code))
-
@add_line_after
def get_subdomain(domain):
@@ -208,61 +284,114 @@ def upload_backup(local_site):
return files_uploaded
-def frappecloud_migrator(local_site, remote_site):
- global login_url, upload_url, files_url, options_url, site_exists_url, session
+def new_site(local_site):
+ # get new site options
+ site_options = get_new_site_options()
+
+ # set preferences from site options
+ subdomain = get_subdomain(site_options["domain"])
+ plan = choose_plan(site_options["plans"])
+
+ app_groups = site_options["groups"]
+ selected_group, filtered_apps = filter_apps(app_groups)
+ files_uploaded = upload_backup(local_site)
+
+ # push to frappe_cloud
+ payload = json.dumps({
+ "site": {
+ "apps": filtered_apps,
+ "files": files_uploaded,
+ "group": selected_group,
+ "name": subdomain,
+ "plan": plan
+ }
+ })
+
+ session.headers.update({"Content-Type": "application/json; charset=utf-8"})
+ site_creation_request = session.post(upload_url, payload)
+
+ if site_creation_request.ok:
+ site_url = site_creation_request.json()["message"]
+ print("Your site {} is being migrated ✨".format(local_site))
+ print("View your site dashboard at {}/dashboard/#/sites/{}".format(remote_site, site_url))
+ print("Your site URL: {}".format(site_url))
+ else:
+ handle_request_failure(site_creation_request)
+
+
+def restore_site(local_site):
+ # get list of existing sites they can restore
+ selected_site = select_site()
+
+ # TODO: check if they can restore it
+
+ click.confirm("This is an irreversible action. Are you sure you want to continue?", abort=True)
+
+ # backup site
+ files_uploaded = upload_backup(local_site)
+
+ # push to frappe_cloud
+ payload = json.dumps({
+ "name": selected_site,
+ "files": files_uploaded
+ })
+ headers = {"Content-Type": "application/json; charset=utf-8"}
+ site_restore_request = session.post(restore_site_url, payload, headers=headers)
+
+ if site_restore_request.ok:
+ print("Your site {0} is being restored on {1} ✨".format(local_site, selected_site))
+ print("View your site dashboard at {}/dashboard/#/sites/{}".format(remote_site, selected_site))
+ print("Your site URL: {}".format(selected_site))
+ else:
+ handle_request_failure(site_restore_request)
+
+
+@add_line_after
+def create_session():
+ print("Frappe Cloud credentials @ {}".format(remote_site))
+
+ # take user input from STDIN
+ username = click.prompt("Username").strip()
+ password = getpass.unix_getpass()
+
+ auth_credentials = {"usr": username, "pwd": password}
+
+ session = requests.Session()
+ login_sc = session.post(login_url, auth_credentials)
+
+ if login_sc.ok:
+ print("Authorization Successful! ✅")
+ team = select_team(session)
+ session.headers.update({"X-Press-Team": team })
+ return session
+ else:
+ handle_request_failure(message="Authorization Failed with Error Code {}".format(login_sc.status_code), traceback=False)
+
+
+def frappecloud_migrator(local_site, frappecloud_site):
+ global login_url, upload_url, files_url, options_url, site_exists_url, restore_site_url, account_details_url, all_site_url
+ global session, migrator_actions, remote_site
+
+ remote_site = frappe.conf.frappecloud_url or "frappecloud.com"
login_url = "https://{}/api/method/login".format(remote_site)
upload_url = "https://{}/api/method/press.api.site.new".format(remote_site)
files_url = "https://{}/api/method/upload_file".format(remote_site)
options_url = "https://{}/api/method/press.api.site.options_for_new".format(remote_site)
site_exists_url = "https://{}/api/method/press.api.site.exists".format(remote_site)
+ account_details_url = "https://{}/api/method/press.api.account.get".format(remote_site)
+ all_site_url = "https://{}/api/method/press.api.site.all".format(remote_site)
+ restore_site_url = "https://{}/api/method/press.api.site.restore".format(remote_site)
- print("Frappe Cloud credentials @ {}".format(remote_site))
+ migrator_actions = [
+ { "title": "Create a new site", "fn": new_site },
+ { "title": "Restore to an existing site", "fn": restore_site }
+ ]
# get credentials + auth user + start session
session = create_session()
- if session:
- # connect to site db
- frappe.init(site=local_site)
- frappe.connect()
+ # available actions defined in migrator_actions
+ primary_action = select_primary_action()
- # get new site options
- site_options = get_new_site_options()
-
- # set preferences from site options
- subdomain = get_subdomain(site_options["domain"])
- plan = choose_plan(site_options["plans"])
-
- app_groups = site_options["groups"]
- selected_group, filtered_apps = filter_apps(app_groups)
- files_uploaded = upload_backup(local_site)
-
- # push to frappe_cloud
- payload = json.dumps({
- "site": {
- "apps": filtered_apps,
- "files": files_uploaded,
- "group": selected_group,
- "name": subdomain,
- "plan": plan
- }
- })
-
- session.headers.update({"Content-Type": "application/json; charset=utf-8"})
- site_creation_request = session.post(upload_url, payload)
- frappe.destroy()
-
- if site_creation_request.ok:
- site_url = site_creation_request.json()["message"]
- print("Your site {} is being migrated ✨".format(local_site))
- print("View your site dashboard at {}/dashboard/#/sites/{}".format(remote_site, site_url))
- print("Your site URL: {}".format(site_url))
- else:
- print("Request failed with error code {}".format(site_creation_request.status_code))
- reason = html2text(site_creation_request.text)
- print(reason)
- sys.exit(1)
-
- else:
- sys.exit(1)
+ primary_action(local_site)
diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py
index 93ef78df7b..3c5d996439 100644
--- a/frappe/model/__init__.py
+++ b/frappe/model/__init__.py
@@ -34,7 +34,8 @@ data_fieldtypes = (
'Signature',
'Color',
'Barcode',
- 'Geolocation'
+ 'Geolocation',
+ 'Duration'
)
no_value_fields = ('Section Break', 'Column Break', 'HTML', 'Table', 'Table MultiSelect', 'Button', 'Image',
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index feeb96898a..106d21eb51 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -693,7 +693,7 @@ class BaseDocument(object):
df = self.meta.get_field(fieldname)
sanitized_value = value
- if df and df.get("fieldtype") in ("Data", "Code", "Small Text") and df.get("options")=="Email":
+ if df and df.get("fieldtype") in ("Data", "Code", "Small Text", "Text") and df.get("options")=="Email":
sanitized_value = sanitize_email(value)
elif df and (df.get("ignore_xss_filter")
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index 596aa18b09..19517aa4a1 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -16,7 +16,7 @@ import frappe, json, copy, re
from frappe.model import optional_fields
from frappe.client import check_parent_permission
from frappe.model.utils.user_settings import get_user_settings, update_user_settings
-from frappe.utils import flt, cint, get_time, make_filter_tuple, get_filter, add_to_date, cstr, nowdate
+from frappe.utils import flt, cint, get_time, make_filter_tuple, get_filter, add_to_date, cstr, get_timespan_date_range
from frappe.model.meta import get_table_columns
class DatabaseQuery(object):
@@ -354,7 +354,9 @@ class DatabaseQuery(object):
ifnull(`tabDocType`.`fieldname`, fallback) operator "value"
"""
- f = get_filter(self.doctype, f)
+ from frappe.boot import get_additional_filters_from_hooks
+ additional_filters_config = get_additional_filters_from_hooks()
+ f = get_filter(self.doctype, f, additional_filters_config)
tname = ('`tab' + f.doctype + '`')
if not tname in self.tables:
@@ -368,6 +370,9 @@ class DatabaseQuery(object):
can_be_null = True
+ if f.operator.lower() in additional_filters_config:
+ f.update(get_additional_filter_field(additional_filters_config, f, f.value))
+
# prepare in condition
if f.operator.lower() in ('ancestors of', 'descendants of', 'not ancestors of', 'not descendants of'):
values = f.value or ''
@@ -426,29 +431,8 @@ class DatabaseQuery(object):
if df and df.fieldtype in ("Check", "Float", "Int", "Currency", "Percent"):
can_be_null = False
- if f.operator.lower() in ('previous', 'next'):
- if f.operator.lower() == "previous":
- if f.value == "1 week":
- date_range = [add_to_date(nowdate(), days=-7), nowdate()]
- elif f.value == "1 month":
- date_range = [add_to_date(nowdate(), months=-1), nowdate()]
- elif f.value == "3 months":
- date_range = [add_to_date(nowdate(), months=-3), nowdate()]
- elif f.value == "6 months":
- date_range = [add_to_date(nowdate(), months=-6), nowdate()]
- elif f.value == "1 year":
- date_range = [add_to_date(nowdate(), years=-1), nowdate()]
- elif f.operator.lower() == "next":
- if f.value == "1 week":
- date_range = [nowdate(), add_to_date(nowdate(), days=7)]
- elif f.value == "1 month":
- date_range = [nowdate(), add_to_date(nowdate(), months=1)]
- elif f.value == "3 months":
- date_range = [nowdate(), add_to_date(nowdate(), months=3)]
- elif f.value == "6 months":
- date_range = [nowdate(), add_to_date(nowdate(), months=6)]
- elif f.value == "1 year":
- date_range = [nowdate(), add_to_date(nowdate(), years=1)]
+ if f.operator.lower() in ('previous', 'next', 'timespan'):
+ date_range = get_date_range(f.operator.lower(), f.value)
f.operator = "Between"
f.value = date_range
fallback = "'0001-01-01 00:00:00'"
@@ -843,4 +827,31 @@ def get_between_date_filter(value, df=None):
frappe.db.format_date(from_date),
frappe.db.format_date(to_date))
- return data
\ No newline at end of file
+ return data
+
+def get_additional_filter_field(additional_filters_config, f, value):
+ additional_filter = additional_filters_config[f.operator.lower()]
+ f = frappe._dict(frappe.get_attr(additional_filter['get_field'])())
+ if f.query_value:
+ for option in f.options:
+ option = frappe._dict(option)
+ if option.value == value:
+ f.value = option.query_value
+ return f
+
+def get_date_range(operator, value):
+ timespan_map = {
+ '1 week': 'week',
+ '1 month': 'month',
+ '3 months': 'quarter',
+ '6 months': '6 months',
+ '1 year': 'year',
+ }
+ period_map = {
+ 'previous': 'last',
+ 'next': 'next',
+ }
+
+ timespan = period_map[operator] + ' ' + timespan_map[value] if operator != 'timespan' else value
+
+ return get_timespan_date_range(timespan)
\ No newline at end of file
diff --git a/frappe/model/mapper.py b/frappe/model/mapper.py
index 3639a947c0..d3014435e0 100644
--- a/frappe/model/mapper.py
+++ b/frappe/model/mapper.py
@@ -14,6 +14,12 @@ def make_mapped_doc(method, source_name, selected_children=None, args=None):
Sets selected_children as flags for the `get_mapped_doc` method.
Called from `open_mapped_doc` from create_new.js'''
+
+ for hook in frappe.get_hooks("override_whitelisted_methods", {}).get(method, []):
+ # override using the first hook
+ method = hook
+ break
+
method = frappe.get_attr(method)
if method not in frappe.whitelisted:
diff --git a/frappe/model/meta.py b/frappe/model/meta.py
index c8fd1a2ac2..1cc3abba5b 100644
--- a/frappe/model/meta.py
+++ b/frappe/model/meta.py
@@ -437,7 +437,7 @@ class Meta(Document):
if not self.custom:
for hook in frappe.get_hooks("override_doctype_dashboards", {}).get(self.name, []):
- data = frappe.get_attr(hook)(data=data)
+ data = frappe._dict(frappe.get_attr(hook)(data=data))
return data
@@ -483,6 +483,9 @@ class Meta(Document):
def get_row_template(self):
return self.get_web_template(suffix='_row')
+ def get_list_template(self):
+ return self.get_web_template(suffix='_list')
+
def get_web_template(self, suffix=''):
'''Returns the relative path of the row template for this doctype'''
module_name = frappe.scrub(self.module)
diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py
index 4491a352bc..1e3f127b99 100644
--- a/frappe/model/rename_doc.py
+++ b/frappe/model/rename_doc.py
@@ -56,6 +56,8 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F
if not merge:
rename_parent_and_child(doctype, old, new, meta)
+ else:
+ update_assignments(old, new, doctype)
# update link fields' values
link_fields = get_link_fields(doctype)
@@ -104,6 +106,27 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F
return new
+def update_assignments(old, new, doctype):
+ old_assignments = frappe.parse_json(frappe.db.get_value(doctype, old, '_assign')) or []
+ new_assignments = frappe.parse_json(frappe.db.get_value(doctype, new, '_assign')) or []
+ common_assignments = list(set(old_assignments).intersection(new_assignments))
+
+ for user in common_assignments:
+ # delete todos linked to old doc
+ todos = frappe.db.get_all('ToDo',
+ {
+ 'owner': user,
+ 'reference_type': doctype,
+ 'reference_name': old,
+ },
+ ['name', 'description']
+ )
+
+ for todo in todos:
+ frappe.delete_doc('ToDo', todo.name)
+
+ unique_assignments = list(set(old_assignments + new_assignments))
+ frappe.db.set_value(doctype, new, '_assign', frappe.as_json(unique_assignments, indent=0))
def update_user_settings(old, new, link_fields):
'''
diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py
index 4384e7c8f5..ea563dfc13 100644
--- a/frappe/model/workflow.py
+++ b/frappe/model/workflow.py
@@ -299,6 +299,7 @@ def set_workflow_state_on_action(doc, workflow_name, action):
return
action_map = {
+ 'update_after_submit': '1',
'submit': '1',
'cancel': '2'
}
diff --git a/frappe/modules/full_text_search.py b/frappe/modules/full_text_search.py
new file mode 100644
index 0000000000..fce9983907
--- /dev/null
+++ b/frappe/modules/full_text_search.py
@@ -0,0 +1,106 @@
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+from whoosh.index import create_in, open_dir
+from whoosh.fields import TEXT, ID, Schema
+from whoosh.qparser import MultifieldParser, FieldsPlugin, WildcardPlugin
+from whoosh.query import Prefix
+from bs4 import BeautifulSoup
+from frappe.website.render import render_page
+from frappe.utils import set_request, cint
+from frappe.utils.global_search import get_routes_to_index
+
+
+def build_index_for_all_routes():
+ print("Building search index for all web routes...")
+ routes = get_routes_to_index()
+ documents = [get_document_to_index(route) for route in routes]
+ build_index("web_routes", documents)
+
+
+@frappe.whitelist(allow_guest=True)
+def web_search(index_name, query, scope=None, limit=20):
+ limit = cint(limit)
+ return search(index_name, query, scope, limit)
+
+
+def get_document_to_index(route):
+ frappe.set_user("Guest")
+ frappe.local.no_cache = True
+
+ try:
+ set_request(method="GET", path=route)
+ content = render_page(route)
+ soup = BeautifulSoup(content, "html.parser")
+ page_content = soup.find(class_="page_content")
+ text_content = page_content.text if page_content else ""
+ title = soup.title.text.strip() if soup.title else route
+
+ frappe.set_user("Administrator")
+
+ return frappe._dict(title=title, content=text_content, path=route)
+ except (
+ frappe.PermissionError,
+ frappe.DoesNotExistError,
+ frappe.ValidationError,
+ Exception,
+ ):
+ pass
+
+
+def build_index(index_name, documents):
+ schema = Schema(
+ title=TEXT(stored=True), path=ID(stored=True), content=TEXT(stored=True)
+ )
+
+ index_dir = get_index_path(index_name)
+ frappe.create_folder(index_dir)
+
+ ix = create_in(index_dir, schema)
+ writer = ix.writer()
+
+ for document in documents:
+ if document:
+ writer.add_document(
+ title=document.title, path=document.path, content=document.content
+ )
+
+ writer.commit()
+
+
+def search(index_name, text, scope=None, limit=20):
+ index_dir = get_index_path(index_name)
+ ix = open_dir(index_dir)
+
+ results = None
+ out = []
+ with ix.searcher() as searcher:
+ parser = MultifieldParser(["title", "content"], ix.schema)
+ parser.remove_plugin_class(FieldsPlugin)
+ parser.remove_plugin_class(WildcardPlugin)
+ query = parser.parse(text)
+
+ filter_scoped = None
+ if scope:
+ filter_scoped = Prefix("path", scope)
+ results = searcher.search(query, limit=limit, filter=filter_scoped)
+
+ for r in results:
+ title_highlights = r.highlights("title")
+ content_highlights = r.highlights("content")
+ out.append(
+ frappe._dict(
+ title=r["title"],
+ path=r["path"],
+ title_highlights=title_highlights,
+ content_highlights=content_highlights,
+ )
+ )
+
+ return out
+
+
+def get_index_path(index_name):
+ return frappe.get_site_path("indexes", index_name)
diff --git a/frappe/patches.txt b/frappe/patches.txt
index 8ab9418e6c..582b369343 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -278,6 +278,14 @@ frappe.patches.v13_0.set_path_for_homepage_in_web_page_view
frappe.patches.v13_0.migrate_translation_column_data
frappe.patches.v13_0.set_read_times
frappe.patches.v13_0.remove_web_view
+frappe.patches.v13_0.site_wise_logging
+frappe.patches.v13_0.set_unique_for_page_view
frappe.patches.v13_0.remove_tailwind_from_page_builder
frappe.patches.v13_0.rename_onboarding
frappe.patches.v13_0.email_unsubscribe
+execute:frappe.delete_doc("Web Template", "Section with Left Image", force=1)
+execute:frappe.delete_doc("DocType", "Onboarding Slide")
+execute:frappe.delete_doc("DocType", "Onboarding Slide Field")
+execute:frappe.delete_doc("DocType", "Onboarding Slide Help Link")
+frappe.patches.v13_0.update_date_filters_in_user_settings
+frappe.patches.v13_0.update_duration_options
diff --git a/frappe/patches/v13_0/set_unique_for_page_view.py b/frappe/patches/v13_0/set_unique_for_page_view.py
new file mode 100644
index 0000000000..2a084e52e3
--- /dev/null
+++ b/frappe/patches/v13_0/set_unique_for_page_view.py
@@ -0,0 +1,6 @@
+import frappe
+
+def execute():
+ frappe.reload_doc('website', 'doctype', 'web_page_view', force=True)
+ site_url = frappe.utils.get_site_url(frappe.local.site)
+ frappe.db.sql("""UPDATE `tabWeb Page View` set is_unique=1 where referrer LIKE '%{0}%'""".format(site_url))
diff --git a/frappe/patches/v13_0/site_wise_logging.py b/frappe/patches/v13_0/site_wise_logging.py
new file mode 100644
index 0000000000..6f04e0c9dd
--- /dev/null
+++ b/frappe/patches/v13_0/site_wise_logging.py
@@ -0,0 +1,10 @@
+import os
+import frappe
+
+
+def execute():
+ site = frappe.local.site
+
+ log_folder = os.path.join(site, 'logs')
+ if not os.path.exists(log_folder):
+ os.mkdir(log_folder)
\ No newline at end of file
diff --git a/frappe/patches/v13_0/update_date_filters_in_user_settings.py b/frappe/patches/v13_0/update_date_filters_in_user_settings.py
new file mode 100644
index 0000000000..d4c6aa1d03
--- /dev/null
+++ b/frappe/patches/v13_0/update_date_filters_in_user_settings.py
@@ -0,0 +1,54 @@
+from __future__ import unicode_literals
+import frappe, json
+from frappe.model.utils.user_settings import update_user_settings, sync_user_settings
+
+def execute():
+ users = frappe.db.sql("select distinct(user) from `__UserSettings`", as_dict=True)
+
+ for user in users:
+ user_settings = frappe.db.sql('''
+ select
+ * from `__UserSettings`
+ where
+ user="{user}"
+ '''.format(user = user.user), as_dict=True)
+
+ for setting in user_settings:
+ data = frappe.parse_json(setting.get('data'))
+ if data:
+ for key in data:
+ update_user_setting_filters(data, key, setting)
+
+ sync_user_settings()
+
+
+def update_user_setting_filters(data, key, user_setting):
+ timespan_map = {
+ '1 week': 'week',
+ '1 month': 'month',
+ '3 months': 'quarter',
+ '6 months': '6 months',
+ '1 year': 'year',
+ }
+
+ period_map = {
+ 'Previous': 'last',
+ 'Next': 'next'
+ }
+
+ if data.get(key):
+ update = False
+ if isinstance(data.get(key), dict):
+ filters = data.get(key).get('filters')
+ if filters and isinstance(filters, list):
+ for f in filters:
+ if f[2] == 'Next' or f[2] == 'Previous':
+ update = True
+ f[3] = period_map[f[2]] + ' ' + timespan_map[f[3]]
+ f[2] = 'Timespan'
+
+ if update:
+ data[key]['filters'] = filters
+ update_user_settings(user_setting['doctype'], json.dumps(data), for_update=True)
+
+
diff --git a/frappe/patches/v13_0/update_duration_options.py b/frappe/patches/v13_0/update_duration_options.py
new file mode 100644
index 0000000000..60eef8fc93
--- /dev/null
+++ b/frappe/patches/v13_0/update_duration_options.py
@@ -0,0 +1,28 @@
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ frappe.reload_doc('core', 'doctype', 'DocField')
+
+ if frappe.db.has_column('DocField', 'show_days'):
+ frappe.db.sql("""
+ UPDATE
+ tabDocField
+ SET
+ hide_days = 1 WHERE show_days = 0
+ """)
+ frappe.db.sql_ddl('alter table tabDocField drop column show_days')
+
+ if frappe.db.has_column('DocField', 'show_seconds'):
+ frappe.db.sql("""
+ UPDATE
+ tabDocField
+ SET
+ hide_seconds = 1 WHERE show_seconds = 0
+ """)
+ frappe.db.sql_ddl('alter table tabDocField drop column show_seconds')
+
+ frappe.clear_cache(doctype='DocField')
\ No newline at end of file
diff --git a/frappe/public/build.json b/frappe/public/build.json
index 30cb2adf87..997a3092ad 100755
--- a/frappe/public/build.json
+++ b/frappe/public/build.json
@@ -250,6 +250,8 @@
"public/less/form_grid.less"
],
"js/form.min.js": [
+ "public/js/frappe/form/templates/address_list.html",
+ "public/js/frappe/form/templates/contact_list.html",
"public/js/frappe/form/templates/print_layout.html",
"public/js/frappe/form/templates/users_in_sidebar.html",
"public/js/frappe/form/templates/set_sharing.html",
diff --git a/frappe/public/css/email.css b/frappe/public/css/email.css
index 92ac433fd2..40c6149927 100644
--- a/frappe/public/css/email.css
+++ b/frappe/public/css/email.css
@@ -1,82 +1,64 @@
/* csslint ignore:start */
-
/* palette colors*/
-
body {
line-height: 1.5;
color: #36414c;
}
-
p {
margin: 1em 0 !important;
}
-
hr {
border-top: 1px solid #d1d8dd;
}
-
.body-table {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
}
-
.body-table td {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
}
-
.email-header,
.email-body,
.email-footer {
width: 100% !important;
min-width: 100% !important;
}
-
.email-body {
font-size: 14px;
}
-
.email-footer {
border-top: 1px solid #d1d8dd;
font-size: 12px;
}
-
.email-header {
border: 1px solid #d1d8dd;
border-radius: 4px 4px 0 0;
}
-
.email-header .brand-image {
width: 24px;
height: 24px;
display: block;
}
-
.email-header-title {
font-weight: bold;
}
-
.body-table.has-header .email-body {
border: 1px solid #d1d8dd;
border-radius: 0 0 4px 4px;
border-top: none;
}
-
.body-table.has-header .email-footer {
border-top: none;
}
-
.email-footer-container {
margin-top: 30px;
}
-
.email-footer-container > div:not(:last-child) {
margin-bottom: 5px;
}
-
.email-unsubscribe a {
color: #8d99a6;
text-decoration: underline;
}
-
.btn {
text-decoration: none;
padding: 7px 10px;
@@ -84,24 +66,20 @@ hr {
border: 1px solid;
border-radius: 3px;
}
-
.btn.btn-default {
color: #fff;
background-color: #f0f4f7;
border-color: transparent;
}
-
.btn.btn-primary {
color: #fff;
background-color: #5e64ff;
border-color: #444bff;
}
-
.table {
width: 100%;
border-collapse: collapse;
}
-
.table td,
.table th {
padding: 8px;
@@ -110,68 +88,53 @@ hr {
border-top: 1px solid #d1d8dd;
text-align: left;
}
-
.table th {
font-weight: bold;
}
-
.table > thead > tr > th {
vertical-align: middle;
border-bottom: 2px solid #d1d8dd;
}
-
.table > thead:first-child > tr:first-child > th {
border-top: none;
}
-
.table.table-bordered {
border: 1px solid #d1d8dd;
}
-
.table.table-bordered td,
.table.table-bordered th {
border: 1px solid #d1d8dd;
}
-
.more-info {
font-size: 80% !important;
color: #8d99a6 !important;
border-top: 1px solid #ebeff2;
padding-top: 10px;
}
-
.text-right {
text-align: right !important;
}
-
.text-center {
text-align: center !important;
}
-
.text-muted {
color: #8d99a6 !important;
}
-
.text-extra-muted {
color: #d1d8dd !important;
}
-
.text-regular {
font-size: 14px;
}
-
.text-medium {
font-size: 12px;
}
-
.text-small {
font-size: 10px;
}
-
.text-bold {
font-weight: bold;
}
-
.indicator {
width: 8px;
height: 8px;
@@ -180,43 +143,33 @@ hr {
display: inline-block;
margin-right: 5px;
}
-
.indicator.indicator-blue {
background-color: #5e64ff;
}
-
.indicator.indicator-green {
background-color: #98d85b;
}
-
.indicator.indicator-orange {
background-color: #ffa00a;
}
-
.indicator.indicator-red {
background-color: #ff5858;
}
-
.indicator.indicator-yellow {
background-color: #feef72;
}
-
.screenshot {
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1);
border: 1px solid #d1d8dd;
margin: 8px 0;
max-width: 100%;
}
-
.list-unstyled {
list-style-type: none;
padding: 0;
}
-
/* auto email report */
-
.report-title {
margin-bottom: 20px;
}
-
/* csslint ignore:end */
diff --git a/frappe/public/css/hljs-night-owl.css b/frappe/public/css/hljs-night-owl.css
new file mode 100644
index 0000000000..932ad2e46f
--- /dev/null
+++ b/frappe/public/css/hljs-night-owl.css
@@ -0,0 +1,183 @@
+/*
+
+Night Owl for highlight.js (c) Carl Baxter
+
+An adaptation of Sarah Drasner's Night Owl VS Code Theme
+https://github.com/sdras/night-owl-vscode-theme
+
+Copyright (c) 2018 Sarah Drasner
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+*/
+
+.hljs {
+ display: block;
+ overflow-x: auto;
+ padding: 1rem 1.25rem;
+ background: #011627;
+ color: #d6deeb;
+ border-radius: 0.5rem;
+ }
+
+ /* General Purpose */
+ .hljs-keyword {
+ color: #c792ea;
+ font-style: italic;
+ }
+ .hljs-built_in {
+ color: #addb67;
+ font-style: italic;
+ }
+ .hljs-type {
+ color: #82aaff;
+ }
+ .hljs-literal {
+ color: #ff5874;
+ }
+ .hljs-number {
+ color: #F78C6C;
+ }
+ .hljs-regexp {
+ color: #5ca7e4;
+ }
+ .hljs-string {
+ color: #ecc48d;
+ }
+ .hljs-subst {
+ color: #d3423e;
+ }
+ .hljs-symbol {
+ color: #82aaff;
+ }
+ .hljs-class {
+ color: #ffcb8b;
+ }
+ .hljs-function {
+ color: #82AAFF;
+ }
+ .hljs-title {
+ color: #DCDCAA;
+ font-style: italic;
+ }
+ .hljs-params {
+ color: #7fdbca;
+ }
+
+ /* Meta */
+ .hljs-comment {
+ color: #637777;
+ font-style: italic;
+ }
+ .hljs-doctag {
+ color: #7fdbca;
+ }
+ .hljs-meta {
+ color: #82aaff;
+ }
+ .hljs-meta-keyword {
+ color: #82aaff;
+ }
+ .hljs-meta-string {
+ color: #ecc48d;
+ }
+
+ /* Tags, attributes, config */
+ .hljs-section {
+ color: #82b1ff;
+ }
+ .hljs-tag,
+ .hljs-name,
+ .hljs-builtin-name {
+ color: #7fdbca;
+ }
+ .hljs-attr {
+ color: #7fdbca;
+ }
+ .hljs-attribute {
+ color: #80cbc4;
+ }
+ .hljs-variable {
+ color: #addb67;
+ }
+
+ /* Markup */
+ .hljs-bullet {
+ color: #d9f5dd;
+ }
+ .hljs-code {
+ color: #80CBC4;
+ }
+ .hljs-emphasis {
+ color: #c792ea;
+ font-style: italic;
+ }
+ .hljs-strong {
+ color: #addb67;
+ font-weight: bold;
+ }
+ .hljs-formula {
+ color: #c792ea;
+ }
+ .hljs-link {
+ color: #ff869a;
+ }
+ .hljs-quote {
+ color: #697098;
+ font-style: italic;
+ }
+
+ /* CSS */
+ .hljs-selector-tag {
+ color: #ff6363;
+ }
+
+ .hljs-selector-id {
+ color: #fad430;
+ }
+
+ .hljs-selector-class {
+ color: #addb67;
+ font-style: italic;
+ }
+
+ .hljs-selector-attr,
+ .hljs-selector-pseudo {
+ color: #c792ea;
+ font-style: italic;
+ }
+
+ /* Templates */
+ .hljs-template-tag {
+ color: #c792ea;
+ }
+ .hljs-template-variable {
+ color: #addb67;
+ }
+
+ /* diff */
+ .hljs-addition {
+ color: #addb67ff;
+ font-style: italic;
+ }
+
+ .hljs-deletion {
+ color: #EF535090;
+ font-style: italic;
+ }
diff --git a/frappe/public/js/frappe/form/controls/control.js b/frappe/public/js/frappe/form/controls/control.js
index 2bf6292abc..168da2717c 100644
--- a/frappe/public/js/frappe/form/controls/control.js
+++ b/frappe/public/js/frappe/form/controls/control.js
@@ -38,6 +38,7 @@ import './table_multiselect';
import './multiselect_pills';
import './multiselect_list';
import './rating';
+import './duration';
frappe.ui.form.make_control = function (opts) {
var control_class_name = "Control" + opts.df.fieldtype.replace(/ /g, "");
diff --git a/frappe/public/js/frappe/form/controls/duration.js b/frappe/public/js/frappe/form/controls/duration.js
new file mode 100644
index 0000000000..e70afd6e65
--- /dev/null
+++ b/frappe/public/js/frappe/form/controls/duration.js
@@ -0,0 +1,152 @@
+frappe.ui.form.ControlDuration = frappe.ui.form.ControlData.extend({
+ make_input: function() {
+ this._super();
+ this.make_picker();
+ },
+
+ make_picker: function() {
+ this.inputs = [];
+ this.set_duration_options();
+ this.$picker = $(
+ ``
+ );
+ this.$wrapper.append(this.$picker);
+ this.build_numeric_input("days", this.duration_options.hide_days);
+ this.build_numeric_input("hours", false);
+ this.build_numeric_input("minutes", false);
+ this.build_numeric_input("seconds", this.duration_options.hide_seconds);
+ this.set_duration_picker_value(this.value);
+ this.$picker.hide();
+ this.bind_events();
+ this.refresh();
+ },
+
+ build_numeric_input: function(label, hidden, max) {
+ let $duration_input = $(`
+
+ `);
+
+ let $input = $(`
`).prepend($duration_input);
+
+ if (max) {
+ $duration_input.attr("max", max);
+ }
+
+ this.inputs[label] = $duration_input;
+
+ let $control = $(`
+ `
+ );
+
+ if (hidden) {
+ $control.addClass("hidden");
+ }
+ $control.prepend($input);
+ $control.appendTo(this.$picker.find(".picker-row"));
+ },
+
+ set_duration_options() {
+ this.duration_options = frappe.utils.get_duration_options(this.df);
+ },
+
+ set_duration_picker_value: function(value) {
+ let total_duration = frappe.utils.seconds_to_duration(value, this.duration_options);
+
+ if (this.$picker) {
+ Object.keys(total_duration).forEach(duration => {
+ this.inputs[duration].prop("value", total_duration[duration]);
+ });
+ }
+ },
+
+ bind_events: function() {
+ // flag to handle the display property of the picker
+ let clicked = false;
+
+ this.$wrapper.find(".duration-input").mousedown(() => {
+ // input in individual duration boxes
+ clicked = true;
+ });
+
+ this.$picker.on("change", ".duration-input", () => {
+ // duration changed in individual boxes
+ clicked = false;
+ let duration = this.get_duration();
+ let value = frappe.utils.duration_to_seconds(
+ duration.days,
+ duration.hours,
+ duration.minutes,
+ duration.seconds
+ );
+ this.set_value(value);
+ this.set_focus();
+ });
+
+ this.$input.on("focus", () => {
+ this.$picker.show();
+ let is_picker_set = this.is_duration_picker_set(this.inputs);
+ if (!is_picker_set) {
+ this.set_duration_picker_value(this.value);
+ }
+ });
+
+ this.$input.on("blur", () => {
+ // input in duration boxes, don't close the picker
+ if (clicked) {
+ clicked = false;
+ } else {
+ // blur event was not due to duration inputs
+ this.$picker.hide();
+ }
+ });
+ },
+
+ get_value() {
+ return cint(this.value);
+ },
+
+ refresh_input: function() {
+ this._super();
+ this.set_duration_options();
+ this.set_duration_picker_value(this.value);
+ },
+
+ format_for_input: function(value) {
+ return frappe.utils.get_formatted_duration(value, this.duration_options);
+ },
+
+ get_duration() {
+ // returns an object of days, hours, minutes and seconds from the inputs array
+ let total_duration = {
+ minutes: 0,
+ hours: 0,
+ days: 0,
+ seconds: 0
+ };
+ if (this.inputs) {
+ total_duration.minutes = parseInt(this.inputs.minutes.val());
+ total_duration.hours = parseInt(this.inputs.hours.val());
+ if (!this.duration_options.hide_days) {
+ total_duration.days = parseInt(this.inputs.days.val());
+ }
+ if (!this.duration_options.hide_seconds) {
+ total_duration.seconds = parseInt(this.inputs.seconds.val());
+ }
+ }
+ return total_duration;
+ },
+
+ is_duration_picker_set(inputs) {
+ let is_set = false;
+ Object.values(inputs).forEach(duration => {
+ if (duration.prop("value") != 0) {
+ is_set = true;
+ }
+ });
+ return is_set;
+ }
+});
\ No newline at end of file
diff --git a/frappe/public/js/frappe/form/controls/multiselect_pills.js b/frappe/public/js/frappe/form/controls/multiselect_pills.js
index 8796c95eaa..6190204357 100644
--- a/frappe/public/js/frappe/form/controls/multiselect_pills.js
+++ b/frappe/public/js/frappe/form/controls/multiselect_pills.js
@@ -129,7 +129,8 @@ frappe.ui.form.ControlMultiSelectPills = frappe.ui.form.ControlAutocomplete.exte
get_data() {
let data;
if(this.df.get_data) {
- data = this.df.get_data();
+ let txt = this.$input.val();
+ data = this.df.get_data(txt);
if (data && data.then) {
data.then((r) => {
this.set_data(r);
diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js
index beec168dfd..bb44408c2a 100644
--- a/frappe/public/js/frappe/form/footer/timeline.js
+++ b/frappe/public/js/frappe/form/footer/timeline.js
@@ -589,7 +589,6 @@ frappe.ui.form.Timeline = class Timeline {
out.push(me.get_version_comment(version, message));
}
} else {
- p = p.map(frappe.utils.escape_html);
const df = frappe.meta.get_docfield(me.frm.doctype, p[0], me.frm.docname);
if (df && !df.hidden) {
const field_display_status = frappe.perm.get_field_display_status(df, null,
@@ -597,8 +596,8 @@ frappe.ui.form.Timeline = class Timeline {
if (field_display_status === 'Read' || field_display_status === 'Write') {
parts.push(__('{0} from {1} to {2}', [
__(df.label),
- (frappe.ellipsis(frappe.utils.html2text(p[1]), 40) || '""').bold(),
- (frappe.ellipsis(frappe.utils.html2text(p[2]), 40) || '""').bold()
+ me.format_content_for_timeline(p[1]),
+ me.format_content_for_timeline(p[2])
]));
}
}
@@ -608,9 +607,9 @@ frappe.ui.form.Timeline = class Timeline {
if (parts.length) {
let message;
if (updater_reference_link) {
- message = __("changed value of {0} {1}", [parts.join(', ').bold(), updater_reference_link]);
+ message = __("changed value of {0} {1}", [parts.join(', '), updater_reference_link]);
} else {
- message = __("changed value of {0}", [parts.join(', ').bold()]);
+ message = __("changed value of {0}", [parts.join(', ')]);
}
out.push(me.get_version_comment(version, message));
}
@@ -618,23 +617,23 @@ frappe.ui.form.Timeline = class Timeline {
// value changed in table field
if (data.row_changed && data.row_changed.length) {
- var parts = [], count = 0;
+ let parts = [];
data.row_changed.every(function(row) {
row[3].every(function(p) {
var df = me.frm.fields_dict[row[0]] &&
frappe.meta.get_docfield(me.frm.fields_dict[row[0]].grid.doctype,
p[0], me.frm.docname);
- if(df && !df.hidden) {
+ if (df && !df.hidden) {
var field_display_status = frappe.perm.get_field_display_status(df,
null, me.frm.perm);
- if(field_display_status === 'Read' || field_display_status === 'Write') {
+ if (field_display_status === 'Read' || field_display_status === 'Write') {
parts.push(__('{0} from {1} to {2} in row #{3}', [
frappe.meta.get_label(me.frm.fields_dict[row[0]].grid.doctype,
p[0]),
- (frappe.ellipsis(p[1], 40) || '""').bold(),
- (frappe.ellipsis(p[2], 40) || '""').bold(),
+ me.format_content_for_timeline(p[1]),
+ me.format_content_for_timeline(p[2]),
row[1]
]));
}
@@ -657,20 +656,22 @@ frappe.ui.form.Timeline = class Timeline {
// rows added / removed
// __('added'), __('removed') # for translation, don't remove
['added', 'removed'].forEach(function(key) {
- if(data[key] && data[key].length) {
- parts = (data[key] || []).map(function(p) {
+ if (data[key] && data[key].length) {
+ let parts = (data[key] || []).map(function(p) {
var df = frappe.meta.get_docfield(me.frm.doctype, p[0], me.frm.docname);
- if(df && !df.hidden) {
+ if (df && !df.hidden) {
var field_display_status = frappe.perm.get_field_display_status(df, null,
me.frm.perm);
- if(field_display_status === 'Read' || field_display_status === 'Write') {
+ if (field_display_status === 'Read' || field_display_status === 'Write') {
return frappe.meta.get_label(me.frm.doctype, p[0])
}
}
});
- parts = parts.filter(function(p) { return p; });
- if(parts.length) {
+ parts = parts.filter(function(p) {
+ return p;
+ });
+ if (parts.length) {
out.push(me.get_version_comment(version, __("{0} rows for {1}",
[__(key), parts.join(', ')])));
}
@@ -717,6 +718,17 @@ frappe.ui.form.Timeline = class Timeline {
}
+ format_content_for_timeline(content) {
+ // text to HTML
+ // limits content to 40 characters
+ // escapes HTML
+ // and makes it bold
+ content = frappe.utils.html2text(content);
+ content = frappe.ellipsis(content, 40) || '""';
+ content = frappe.utils.escape_html(content);
+ return content.bold();
+ }
+
delete_comment(name) {
var me = this;
diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js
index bad7c877fc..c09d7b06ff 100644
--- a/frappe/public/js/frappe/form/form.js
+++ b/frappe/public/js/frappe/form/form.js
@@ -650,13 +650,14 @@ frappe.ui.form.Form = class FrappeForm {
frappe.utils.play_sound("submit");
callback && callback();
me.script_manager.trigger("on_submit")
- .then(() => resolve(me));
- if (frappe.route_hooks.after_submit) {
- let route_callback = frappe.route_hooks.after_submit;
- delete frappe.route_hooks.after_submit;
-
- route_callback(me);
- }
+ .then(() => resolve(me))
+ .then(() => {
+ if (frappe.route_hooks.after_submit) {
+ let route_callback = frappe.route_hooks.after_submit;
+ delete frappe.route_hooks.after_submit;
+ route_callback(me);
+ }
+ });
}
}, btn, () => me.handle_save_fail(btn, on_error), resolve);
});
@@ -786,15 +787,24 @@ frappe.ui.form.Form = class FrappeForm {
frappe.msgprint(__('"amended_from" field must be present to do an amendment.'));
return;
}
- this.validate_form_action("Amend");
- var me = this;
- var fn = function(newdoc) {
- newdoc.amended_from = me.docname;
- if(me.fields_dict && me.fields_dict['amendment_date'])
- newdoc.amendment_date = frappe.datetime.obj_to_str(new Date());
- };
- this.copy_doc(fn, 1);
- frappe.utils.play_sound("click");
+
+ frappe.xcall('frappe.client.is_document_amended', {
+ 'doctype': this.doc.doctype,
+ 'docname': this.doc.name
+ }).then(is_amended => {
+ if (is_amended) {
+ frappe.throw(__('This document is already amended, you cannot ammend it again'));
+ }
+ this.validate_form_action("Amend");
+ var me = this;
+ var fn = function(newdoc) {
+ newdoc.amended_from = me.docname;
+ if (me.fields_dict && me.fields_dict['amendment_date'])
+ newdoc.amendment_date = frappe.datetime.obj_to_str(new Date());
+ };
+ this.copy_doc(fn, 1);
+ frappe.utils.play_sound("click");
+ });
}
validate_form_action(action, resolve) {
@@ -1586,7 +1596,7 @@ frappe.ui.form.Form = class FrappeForm {
let steps = frappe.tour[this.doctype].map(step => {
let field = this.get_docfield(step.fieldname);
return {
- element: `.frappe-control[title='${step.fieldname}']`,
+ element: `.frappe-control[data-fieldname='${step.fieldname}']`,
popover: {
title: step.title || field.label,
description: step.description
diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js
index d178c59100..9f4a2a61d6 100644
--- a/frappe/public/js/frappe/form/formatters.js
+++ b/frappe/public/js/frappe/form/formatters.js
@@ -142,10 +142,7 @@ frappe.form.formatters = {
},
DateRange: function(value) {
if($.isArray(value)) {
- return __("{0} to {1}", [
- frappe.datetime.str_to_user(value[0]),
- frappe.datetime.str_to_user(value[1])
- ]);
+ return __("{0} to {1}", [frappe.datetime.str_to_user(value[0]), frappe.datetime.str_to_user(value[1])]);
} else {
return value || "";
}
@@ -188,6 +185,14 @@ frappe.form.formatters = {
return value || "";
},
+ Duration: function(value, docfield) {
+ if (value) {
+ let duration_options = frappe.utils.get_duration_options(docfield);
+ value = frappe.utils.get_formatted_duration(value, duration_options);
+ }
+
+ return value || "";
+ },
LikedBy: function(value) {
var html = "";
$.each(JSON.parse(value || "[]"), function(i, v) {
diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js
index cdd385a6ea..41b87e0207 100644
--- a/frappe/public/js/frappe/form/multi_select_dialog.js
+++ b/frappe/public/js/frappe/form/multi_select_dialog.js
@@ -1,110 +1,62 @@
-// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-// MIT License. See license.txt
-
-frappe.ui.form.MultiSelectDialog = Class.extend({
- init: function(opts) {
- /* Options: doctype, target, setters, get_query, action */
- $.extend(this, opts);
-
+frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
+ constructor(opts) {
+ /* Options: doctype, target, setters, get_query, action, add_filters_group, data_fields, primary_action_label */
+ Object.assign(this, opts);
var me = this;
- if(this.doctype!="[Select]") {
- frappe.model.with_doctype(this.doctype, function(r) {
+ if (this.doctype != "[Select]") {
+ frappe.model.with_doctype(this.doctype, function () {
me.make();
});
} else {
this.make();
}
- },
- make: function() {
- let me = this;
+ }
+ make() {
+ let me = this;
this.page_length = 20;
this.start = 0;
+ let fields = this.get_primary_filters();
- let fields = [
- {
- fieldtype: "Data",
- label: __("Search Term"),
- fieldname: "search_term"
- },
- {
- fieldtype: "Column Break"
- }
- ];
- let count = 0;
- if(!this.date_field) {
- this.date_field = "transaction_date";
- }
-
- // setters can be defined as a dict or a list of fields
- // setters define the additional filters that get applied
- // for selection
-
- // CASE 1: DocType name and fieldname is the same, example "customer" and "customer"
- // setters define the filters applied in the modal
- // if the fieldnames and doctypes are consistently named,
- // pass a dict with the setter key and value, for example
- // {customer: [customer_name]}
-
- // CASE 2: if the fieldname of the target is different,
- // then pass a list of fields with appropriate fieldname
-
- if($.isArray(this.setters)) {
- for (let df of this.setters) {
- fields.push(df, {fieldtype: "Column Break"});
- }
- } else {
- Object.keys(this.setters).forEach(function(setter) {
- fields.push({
- fieldtype: me.target.fields_dict[setter].df.fieldtype,
- label: me.target.fields_dict[setter].df.label,
- fieldname: setter,
- options: me.target.fields_dict[setter].df.options,
- default: me.setters[setter]
- });
- if (count++ < Object.keys(me.setters).length) {
- fields.push({fieldtype: "Column Break"});
- }
- });
- }
-
+ // Make results area
fields = fields.concat([
- {
- "fieldname":"date_range",
- "label": __("Date Range"),
- "fieldtype": "DateRange",
- },
- { fieldtype: "Section Break" },
{ fieldtype: "HTML", fieldname: "results_area" },
- { fieldtype: "Button", fieldname: "more_btn", label: __("More"),
- click: function(){
- me.start += 20;
- frappe.flags.auto_scroll = true;
- me.get_results();
+ {
+ fieldtype: "Button", fieldname: "more_btn", label: __("More"),
+ click: () => {
+ this.start += 20;
+ this.get_results();
}
}
]);
- let doctype_plural = !this.doctype.endsWith('y') ? this.doctype + 's'
- : this.doctype.slice(0, -1) + 'ies';
+ // Custom Data Fields
+ if (this.data_fields) {
+ fields.push({ fieldtype: "Section Break" });
+ fields = fields.concat(this.data_fields);
+ }
+
+ let doctype_plural = this.doctype.plural();
+
this.dialog = new frappe.ui.Dialog({
- title: __("Select {0}", [(this.doctype=='[Select]') ? __("value") : __(doctype_plural)]),
+ title: __("Select {0}", [(this.doctype == '[Select]') ? __("value") : __(doctype_plural)]),
fields: fields,
- primary_action_label: __("Get Items"),
+ primary_action_label: this.primary_action_label || __("Get Items"),
secondary_action_label: __("Make {0}", [me.doctype]),
- primary_action: function() {
- me.action(me.get_checked_values(), me.args);
+ primary_action: function () {
+ let filters_data = me.get_custom_filters();
+ me.action(me.get_checked_values(), cur_dialog.get_values(), me.args, filters_data);
},
- secondary_action: function(e) {
+ secondary_action: function (e) {
// If user wants to close the modal
if (e) {
frappe.route_options = {};
- if($.isArray(me.setters)) {
+ if (Array.isArray(me.setters)) {
for (let df of me.setters) {
frappe.route_options[df.fieldname] = me.dialog.fields_dict[df.fieldname].get_value() || undefined;
}
} else {
- Object.keys(me.setters).forEach(function(setter) {
+ Object.keys(me.setters).forEach(function (setter) {
frappe.route_options[setter] = me.dialog.fields_dict[setter].get_value() || undefined;
});
}
@@ -114,6 +66,10 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
}
});
+ if (this.add_filters_group) {
+ this.make_filter_area();
+ }
+
this.$parent = $(this.dialog.body);
this.$wrapper = this.dialog.fields_dict.results_area.$wrapper.append(`
`);
@@ -126,9 +82,89 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
this.bind_events();
this.get_results();
this.dialog.show();
- },
+ }
- bind_events: function() {
+ get_primary_filters() {
+ let fields = [];
+
+ let columns = new Array(3);
+
+ // Hack for three column layout
+ // To add column break
+ columns[0] = [
+ {
+ fieldtype: "Data",
+ label: __("Search"),
+ fieldname: "search_term"
+ }
+ ];
+ columns[1] = [];
+ columns[2] = [];
+
+ Object.keys(this.setters).forEach((setter, index) => {
+ let df_prop = frappe.meta.docfield_map[this.doctype][setter];
+
+ // Index + 1 to start filling from index 1
+ // Since Search is a standrd field already pushed
+ columns[(index + 1) % 3].push({
+ fieldtype: df_prop.fieldtype,
+ label: df_prop.label,
+ fieldname: setter,
+ options: df_prop.options,
+ default: this.setters[setter]
+ });
+ });
+
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal
+ if (Object.seal) {
+ Object.seal(columns);
+ // now a is a fixed-size array with mutable entries
+ }
+
+ fields = [
+ ...columns[0],
+ { fieldtype: "Column Break" },
+ ...columns[1],
+ { fieldtype: "Column Break" },
+ ...columns[2],
+ { fieldtype: "Section Break", fieldname: "primary_filters_sb" }
+ ];
+
+ if (this.add_filters_group) {
+ fields.push(
+ {
+ fieldtype: 'HTML',
+ fieldname: 'filter_area',
+ }
+ );
+ }
+
+ return fields;
+ }
+
+ make_filter_area() {
+ this.filter_group = new frappe.ui.FilterGroup({
+ parent: this.dialog.get_field('filter_area').$wrapper,
+ doctype: this.doctype,
+ on_change: () => {
+ this.get_results();
+ }
+ });
+ }
+
+ get_custom_filters() {
+ if (this.add_filters_group && this.filter_group) {
+ return this.filter_group.get_filters().reduce((acc, filter) => {
+ return Object.assign(acc, {
+ [filter[1]]: [filter[2], filter[3]]
+ });
+ }, {});
+ } else {
+ return [];
+ }
+ }
+
+ bind_events() {
let me = this;
this.$results.on('click', '.list-item-container', function (e) {
@@ -136,48 +172,44 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
$(this).find(':checkbox').trigger('click');
}
});
+
this.$results.on('click', '.list-item--head :checkbox', (e) => {
this.$results.find('.list-item-container .list-row-check')
.prop("checked", ($(e.target).is(':checked')));
});
- this.$parent.find('.input-with-feedback').on('change', (e) => {
+ this.$parent.find('.input-with-feedback').on('change', () => {
frappe.flags.auto_scroll = false;
this.get_results();
});
- this.$parent.find('[data-fieldname="date_range"]').on('blur', (e) => {
- frappe.flags.auto_scroll = false;
- this.get_results();
- });
-
- this.$parent.find('[data-fieldname="search_term"]').on('input', (e) => {
+ this.$parent.find('[data-fieldtype="Data"]').on('input', () => {
var $this = $(this);
clearTimeout($this.data('timeout'));
- $this.data('timeout', setTimeout(function() {
+ $this.data('timeout', setTimeout(function () {
frappe.flags.auto_scroll = false;
me.empty_list();
me.get_results();
}, 300));
});
- },
+ }
- get_checked_values: function() {
+ get_checked_values() {
// Return name of checked value.
- return this.$results.find('.list-item-container').map(function() {
- if ($(this).find('.list-row-check:checkbox:checked').length > 0 ) {
+ return this.$results.find('.list-item-container').map(function () {
+ if ($(this).find('.list-row-check:checkbox:checked').length > 0) {
return $(this).attr('data-item-name');
}
}).get();
- },
+ }
- get_checked_items: function() {
+ get_checked_items() {
// Return checked items with all the column values.
let checked_values = this.get_checked_values();
return this.results.filter(res => checked_values.includes(res.name));
- },
+ }
- make_list_row: function(result={}) {
+ make_list_row(result = {}) {
var me = this;
// Make a head row by default (if result not passed)
let head = Object.keys(result).length === 0;
@@ -185,26 +217,17 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
let contents = ``;
let columns = ["name"];
- if($.isArray(this.setters)) {
- for (let df of this.setters) {
- columns.push(df.fieldname);
- }
- } else {
- columns = columns.concat(Object.keys(this.setters));
- }
- columns.push("Date");
+ columns = columns.concat(Object.keys(this.setters));
- columns.forEach(function(column) {
+ columns.forEach(function (column) {
contents += `
${
- head ? `
${__(frappe.model.unscrub(column))} `
-
- : (column !== "name" ? `
${__(result[column])} `
- : `
- ${__(result[column])} `)
- }
+ head ? `
${__(frappe.model.unscrub(column))} `
+ : (column !== "name" ? `
${__(result[column] || '')} `
+ : `
+ ${__(result[column] || '')} `)}
`;
- })
+ });
let $row = $(`
@@ -215,10 +238,12 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
head ? $row.addClass('list-item--head')
: $row = $(`
`).append($row);
- return $row;
- },
- render_result_list: function(results, more = 0, empty=true) {
+ $(".modal-dialog .list-item--head").css("z-index", 0);
+ return $row;
+ }
+
+ render_result_list(results, more = 0, empty = true) {
var me = this;
var more_btn = me.dialog.fields_dict.more_btn.$wrapper;
@@ -240,44 +265,44 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
});
if (frappe.flags.auto_scroll) {
- this.$results.animate({scrollTop: me.$results.prop('scrollHeight')}, 500);
+ this.$results.animate({ scrollTop: me.$results.prop('scrollHeight') }, 500);
}
- },
+ }
- empty_list: function() {
+ empty_list() {
+ // Store all checked items
let checked = this.get_checked_items().map(item => {
return {
...item,
checked: true
- }
+ };
});
+
+ // Remove **all** items
this.$results.find('.list-item-container').remove();
+
+ // Rerender checked items
this.render_result_list(checked, 0, false);
- },
+ }
- get_results: function() {
+ get_results() {
let me = this;
+ let filters = this.get_query ? this.get_query().filters : {} || {};
+ let filter_fields = [];
- let filters = this.get_query ? this.get_query().filters : {};
- let filter_fields = [me.date_field];
- if($.isArray(this.setters)) {
- for (let df of this.setters) {
- filters[df.fieldname] = me.dialog.fields_dict[df.fieldname].get_value() || undefined;
- me.args[df.fieldname] = filters[df.fieldname];
- filter_fields.push(df.fieldname);
- }
- } else {
- Object.keys(this.setters).forEach(function(setter) {
- filters[setter] = me.dialog.fields_dict[setter].get_value() || undefined;
+ Object.keys(this.setters).forEach(function (setter) {
+ var value = me.dialog.fields_dict[setter].get_value();
+ if (me.dialog.fields_dict[setter].df.fieldtype == "Data" && value) {
+ filters[setter] = ["like", "%" + value + "%"];
+ } else {
+ filters[setter] = value || undefined;
me.args[setter] = filters[setter];
filter_fields.push(setter);
- });
- }
+ }
+ });
- let date_val = this.dialog.fields_dict["date_range"].get_value();
- if(date_val) {
- filters[this.date_field] = ['between', date_val];
- }
+ let filter_group = this.get_custom_filters();
+ Object.assign(filters, filter_group);
let args = {
doctype: me.doctype,
@@ -288,13 +313,13 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
page_length: this.page_length + 1,
query: this.get_query ? this.get_query().query : '',
as_dict: 1
- }
+ };
frappe.call({
type: "GET",
- method:'frappe.desk.search.search_widget',
+ method: 'frappe.desk.search.search_widget',
no_spinner: true,
args: args,
- callback: function(r) {
+ callback: function (r) {
let more = 0;
me.results = [];
if (r.values.length) {
@@ -302,30 +327,13 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
r.values.pop();
more = 1;
}
- r.values.forEach(function(result) {
- if(me.date_field in result) {
- result["Date"] = result[me.date_field]
- }
+ r.values.forEach(function (result) {
result.checked = 0;
- result.parsed_date = Date.parse(result["Date"]);
me.results.push(result);
});
- me.results.map( (result) => {
- result["Date"] = frappe.format(result["Date"], {"fieldtype":"Date"});
- })
-
- me.results.sort((a, b) => {
- return a.parsed_date - b.parsed_date;
- });
-
- // Preselect oldest entry
- if (me.start < 1 && r.values.length === 1) {
- me.results[0].checked = 1;
- }
}
me.render_result_list(me.results, more);
}
});
- },
-
-});
\ No newline at end of file
+ }
+};
\ No newline at end of file
diff --git a/frappe/public/js/frappe/form/script_manager.js b/frappe/public/js/frappe/form/script_manager.js
index fd5b8d3856..1238bf141c 100644
--- a/frappe/public/js/frappe/form/script_manager.js
+++ b/frappe/public/js/frappe/form/script_manager.js
@@ -16,12 +16,22 @@ frappe.ui.form.get_event_handler_list = function(doctype, fieldname) {
frappe.ui.form.on = frappe.ui.form.on_change = function(doctype, fieldname, handler) {
var add_handler = function(fieldname, handler) {
var handler_list = frappe.ui.form.get_event_handler_list(doctype, fieldname);
- handler_list.push(handler);
+
+ let _handler = (...args) => {
+ try {
+ handler(...args);
+ } catch (error) {
+ console.error(handler);
+ throw error;
+ }
+ }
+
+ handler_list.push(_handler);
// add last handler to events so it can be called as
// frm.events.handler(frm)
if(cur_frm && cur_frm.doctype===doctype) {
- cur_frm.events[fieldname] = handler;
+ cur_frm.events[fieldname] = _handler;
}
}
diff --git a/frappe/public/js/frappe/form/templates/address_list.html b/frappe/public/js/frappe/form/templates/address_list.html
new file mode 100644
index 0000000000..0f967b67a0
--- /dev/null
+++ b/frappe/public/js/frappe/form/templates/address_list.html
@@ -0,0 +1,22 @@
+
+{% for(var i=0, l=addr_list.length; i
+
+ {%= i+1 %}. {%= addr_list[i].address_title %}{% if(addr_list[i].address_type!="Other") { %}
+ ({%= __(addr_list[i].address_type) %}) {% } %}
+ {% if(addr_list[i].is_primary_address) { %}
+ ({%= __("Primary") %}) {% } %}
+ {% if(addr_list[i].is_shipping_address) { %}
+ ({%= __("Shipping") %}) {% } %}
+
+
+ {%= __("Edit") %}
+
+ {%= addr_list[i].display %}
+
+{% } %}
+{% if(!addr_list.length) { %}
+
{%= __("No address added yet.") %}
+{% } %}
+
{{ __("New Address") }}
\ No newline at end of file
diff --git a/frappe/public/js/frappe/form/templates/contact_list.html b/frappe/public/js/frappe/form/templates/contact_list.html
new file mode 100644
index 0000000000..7e6969163b
--- /dev/null
+++ b/frappe/public/js/frappe/form/templates/contact_list.html
@@ -0,0 +1,54 @@
+
+{% for(var i=0, l=contact_list.length; i
+
+ {%= contact_list[i].first_name %} {%= contact_list[i].last_name %}
+ {% if(contact_list[i].is_primary_contact) { %}
+ ({%= __("Primary") %})
+ {% } %}
+ {% if(contact_list[i].designation){ %}
+ – {%= contact_list[i].designation %}
+ {% } %}
+
+ {%= __("Edit") %}
+
+ {% if (contact_list[i].phones || contact_list[i].email_ids) { %}
+
+ {% if(contact_list[i].phone) { %}
+ {%= __("Phone") %}: {%= contact_list[i].phone %} ({%= __("Primary") %})
+ {% endif %}
+ {% if(contact_list[i].mobile_no) { %}
+ {%= __("Mobile No") %}: {%= contact_list[i].mobile_no %} ({%= __("Primary") %})
+ {% endif %}
+ {% if(contact_list[i].phone_nos) { %}
+ {% for(var j=0, k=contact_list[i].phone_nos.length; j
+ {% } %}
+ {% endif %}
+
+
+ {% if(contact_list[i].email_id) { %}
+ {%= __("Email") %}: {%= contact_list[i].email_id %} ({%= __("Primary") %})
+ {% endif %}
+ {% if(contact_list[i].email_ids) { %}
+ {% for(var j=0, k=contact_list[i].email_ids.length; j
+ {% } %}
+ {% endif %}
+
+ {% endif %}
+
+ {% if (contact_list[i].address) { %}
+ {%= __("Address") %}: {%= contact_list[i].address %}
+ {% endif %}
+
+
+{% } %}
+{% if(!contact_list.length) { %}
+{%= __("No contacts added yet.") %}
+{% } %}
+
+ {{ __("New Contact") }}
+
\ No newline at end of file
diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js
index 528c874935..6f475fa9e5 100644
--- a/frappe/public/js/frappe/form/toolbar.js
+++ b/frappe/public/js/frappe/form/toolbar.js
@@ -374,19 +374,24 @@ frappe.ui.form.Toolbar = Class.extend({
var status = this.get_action_status();
if (status) {
- if (status !== this.current_status) {
- if (status === 'Amend') {
- let doc = this.frm.doc;
- frappe.xcall('frappe.client.is_document_amended', {
- 'doctype': doc.doctype,
- 'docname': doc.name
- }).then(is_amended => {
- if (is_amended) return;
- this.set_page_actions(status);
- });
- } else {
+ // When moving from a page with status amend to another page with status amend
+ // We need to check if document is already amened specifcally and hide
+ // or clear the menu actions accordingly
+
+ if (status !== this.current_status || status === 'Amend') {
+ let doc = this.frm.doc;
+ frappe.xcall('frappe.client.is_document_amended', {
+ 'doctype': doc.doctype,
+ 'docname': doc.name
+ }).then(is_amended => {
+ if (is_amended) {
+ this.page.clear_actions();
+ return;
+ }
this.set_page_actions(status);
- }
+ });
+ } else {
+ this.set_page_actions(status);
}
} else {
this.page.clear_actions();
diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js
index 15f77fada5..b94257106e 100644
--- a/frappe/public/js/frappe/list/base_list.js
+++ b/frappe/public/js/frappe/list/base_list.js
@@ -338,6 +338,11 @@ frappe.views.BaseList = class BaseList {
: [];
}
+ get_filter_value(fieldname) {
+ return this.get_filters_for_args().filter(f => f[1] == fieldname)[0] &&
+ this.get_filters_for_args().filter(f => f[1] == fieldname)[0][3];
+ }
+
get_args() {
return {
doctype: this.doctype,
diff --git a/frappe/public/js/frappe/list/list_settings.js b/frappe/public/js/frappe/list/list_settings.js
new file mode 100644
index 0000000000..f2045c9c34
--- /dev/null
+++ b/frappe/public/js/frappe/list/list_settings.js
@@ -0,0 +1,383 @@
+export default class ListSettings {
+ constructor({ listview, doctype, meta, settings }) {
+ if (!doctype) {
+ frappe.throw(__('Doctype required'));
+ }
+
+ this.listview = listview;
+ this.doctype = doctype;
+ this.meta = meta;
+ this.settings = settings;
+ this.dialog = null;
+ this.fields = this.settings && this.settings.fields ? JSON.parse(this.settings.fields) : [];
+ this.subject_field = null;
+
+ frappe.model.with_doctype("List View Settings", () => {
+ this.make();
+ this.get_listview_fields(meta);
+ this.setup_fields();
+ this.setup_remove_fields();
+ this.add_new_fields();
+ this.show_dialog();
+ });
+ }
+
+ make() {
+ let me = this;
+
+ let list_view_settings = frappe.get_meta("List View Settings");
+
+ me.dialog = new frappe.ui.Dialog({
+ title: __("{0} Settings", [__(me.doctype)]),
+ fields: list_view_settings.fields
+ });
+ me.dialog.set_values(me.settings);
+ me.dialog.set_primary_action(__('Save'), () => {
+ let values = me.dialog.get_values();
+
+ frappe.show_alert({
+ message: __("Saving"),
+ indicator: "green"
+ });
+
+ frappe.call({
+ method: "frappe.desk.doctype.list_view_settings.list_view_settings.save_listview_settings",
+ args: {
+ doctype: me.doctype,
+ listview_settings: values,
+ removed_listview_fields: me.removed_fields || []
+ },
+ callback: function (r) {
+ me.listview.refresh_columns(r.message.meta, r.message.listview_settings);
+ me.dialog.hide();
+ }
+ });
+ });
+
+ me.dialog.fields_dict["total_fields"].df.onchange = () => me.refresh();
+ }
+
+ refresh() {
+ let me = this;
+
+ me.setup_fields();
+ me.add_new_fields();
+ me.setup_remove_fields();
+ }
+
+ show_dialog() {
+ let me = this;
+
+ if (!this.settings.fields) {
+ me.update_fields();
+ }
+
+ if (!me.dialog.get_value("total_fields")) {
+ let field_count = me.fields.length;
+
+ if (field_count < 4) {
+ field_count = 4;
+ } else if (field_count > 10) {
+ field_count = 4;
+ }
+
+ me.dialog.set_value("total_fields", field_count);
+ }
+
+ me.dialog.show();
+ }
+
+ setup_fields() {
+ function is_status_field(field) {
+ return field.fieldname === "status_field";
+ }
+
+ let me = this;
+
+ let fields_html = me.dialog.get_field("fields_html");
+ let wrapper = fields_html.$wrapper[0];
+ let fields = ``;
+ let total_fields = me.dialog.get_values().total_fields || me.settings.total_fields;
+
+ for (let idx in me.fields) {
+ if (idx == parseInt(total_fields)) {
+ break;
+ }
+ let is_sortable = (idx == 0) ? `` : `sortable`;
+ let show_sortable_handle = (idx == 0) ? `hide` : ``;
+ let can_remove = (idx == 0 || is_status_field(me.fields[idx])) ? `hide` : ``;
+
+ fields += `
+ `;
+ }
+
+ fields_html.html(`
+
+ `);
+
+ new Sortable(wrapper.getElementsByClassName("control-input-wrapper")[0], {
+ handle: '.sortable-handle',
+ draggable: '.sortable',
+ onUpdate: () => {
+ me.update_fields();
+ me.refresh();
+ }
+ });
+ }
+
+ add_new_fields() {
+ let me = this;
+
+ let fields_html = me.dialog.get_field("fields_html");
+ let add_new_fields = fields_html.$wrapper[0].getElementsByClassName("add-new-fields")[0];
+ add_new_fields.onclick = () => me.column_selector();
+ }
+
+ setup_remove_fields() {
+ let me = this;
+
+ let fields_html = me.dialog.get_field("fields_html");
+ let remove_fields = fields_html.$wrapper[0].getElementsByClassName("remove-field");
+
+ for (let idx = 0; idx < remove_fields.length; idx++) {
+ remove_fields.item(idx).onclick = () => me.remove_fields(remove_fields.item(idx).getAttribute("data-fieldname"));
+ }
+ }
+
+ remove_fields(fieldname) {
+ let me = this;
+ let existing_fields = me.fields.map(f => f.fieldname);
+
+ for (let idx in me.fields) {
+ let field = me.fields[idx];
+
+ if (field.fieldname == fieldname) {
+ me.fields.splice(idx, 1);
+ break;
+ }
+ }
+ me.set_removed_fields(me.get_removed_listview_fields(me.fields.map(f => f.fieldname), existing_fields));
+ me.refresh();
+ me.update_fields();
+ }
+
+ update_fields() {
+ let me = this;
+
+ let fields_html = me.dialog.get_field("fields_html");
+ let wrapper = fields_html.$wrapper[0];
+
+ let fields_order = wrapper.getElementsByClassName("fields_order");
+ me.fields = [];
+
+ for (let idx = 0; idx < fields_order.length; idx++) {
+ me.fields.push({
+ fieldname: fields_order.item(idx).getAttribute("data-fieldname"),
+ label: fields_order.item(idx).getAttribute("data-label")
+ });
+ }
+
+ me.dialog.set_value("fields", JSON.stringify(me.fields));
+ me.dialog.get_value("fields");
+ }
+
+ column_selector() {
+ let me = this;
+
+ let d = new frappe.ui.Dialog({
+ title: __("{0} Fields", [__(me.doctype)]),
+ fields: [
+ {
+ label: __("Reset Fields"),
+ fieldtype: "Button",
+ fieldname: "reset_fields",
+ click: () => me.reset_listview_fields(d)
+ },
+ {
+ label: __("Select Fields"),
+ fieldtype: "MultiCheck",
+ fieldname: "fields",
+ options: me.get_doctype_fields(me.meta, me.fields.map(f => f.fieldname)),
+ columns: 2
+ }
+ ]
+ });
+ d.set_primary_action(__('Save'), () => {
+ let values = d.get_values().fields;
+
+ me.set_removed_fields(me.get_removed_listview_fields(values, me.fields.map(f => f.fieldname)));
+
+ me.fields = [];
+ me.set_subject_field(me.meta);
+ me.set_status_field();
+
+ for (let idx in values) {
+ let value = values[idx];
+
+ if (me.fields.length === parseInt(me.dialog.get_values().total_fields)) {
+ break;
+ } else if (value != me.subject_field.fieldname) {
+ let field = frappe.meta.get_docfield(me.doctype, value);
+ if (field) {
+ me.fields.push({
+ label: field.label,
+ fieldname: field.fieldname
+ });
+ }
+ }
+ }
+
+ me.refresh();
+ me.dialog.set_value("fields", JSON.stringify(me.fields));
+ d.hide();
+ });
+ d.show();
+ }
+
+ reset_listview_fields(dialog) {
+ let me = this;
+
+ frappe.xcall("frappe.desk.doctype.list_view_settings.list_view_settings.get_default_listview_fields", {
+ doctype: me.doctype
+ }).then((fields) => {
+ let field = dialog.get_field("fields");
+ field.df.options = me.get_doctype_fields(me.meta, fields);
+ dialog.refresh();
+ });
+
+ }
+
+ get_listview_fields(meta) {
+ let me = this;
+
+ if (!me.settings.fields) {
+ me.set_list_view_fields(meta);
+ } else {
+ me.fields = JSON.parse(this.settings.fields);
+ }
+
+ me.fields.uniqBy(f => f.fieldname);
+ }
+
+ set_list_view_fields(meta) {
+ let me = this;
+
+ me.set_subject_field(meta);
+ me.set_status_field();
+
+ meta.fields.forEach(field => {
+ if (field.in_list_view && !in_list(frappe.model.no_value_type, field.fieldtype) &&
+ me.subject_field.fieldname != field.fieldname) {
+
+ me.fields.push({
+ label: field.label,
+ fieldname: field.fieldname
+ });
+ }
+ });
+ }
+
+ set_subject_field(meta) {
+ let me = this;
+
+ me.subject_field = {
+ label: "Name",
+ fieldname: "name"
+ };
+
+ if (meta.title_field) {
+ let field = frappe.meta.get_docfield(me.doctype, meta.title_field.trim());
+
+ me.subject_field = {
+ label: field.label,
+ fieldname: field.fieldname
+ };
+ }
+
+ me.fields.push(me.subject_field);
+ }
+
+ set_status_field() {
+ let me = this;
+
+ if (frappe.has_indicator(me.doctype)) {
+ me.fields.push({
+ type: "Status",
+ label: "Status",
+ fieldname: "status_field"
+ });
+ }
+ }
+
+ get_doctype_fields(meta, fields) {
+ let multiselect_fields = [];
+
+ meta.fields.forEach(field => {
+ if (!in_list(frappe.model.no_value_type, field.fieldtype)) {
+ multiselect_fields.push({
+ label: field.label,
+ value: field.fieldname,
+ checked: in_list(fields, field.fieldname)
+ });
+ }
+ });
+
+ return multiselect_fields;
+ }
+
+ get_removed_listview_fields(new_fields, existing_fields) {
+ let me = this;
+ let removed_fields = [];
+
+ if (frappe.has_indicator(me.doctype)) {
+ new_fields.push("status_field");
+ }
+
+ existing_fields.forEach(column => {
+ if (!in_list(new_fields, column)) {
+ removed_fields.push(column);
+ }
+ });
+
+ return removed_fields;
+ }
+
+ set_removed_fields(fields) {
+ let me = this;
+
+ if (me.removed_fields) {
+ me.removed_fields.concat(fields);
+ } else {
+ me.removed_fields = fields;
+ }
+ }
+}
diff --git a/frappe/public/js/frappe/list/list_sidebar_group_by.js b/frappe/public/js/frappe/list/list_sidebar_group_by.js
index 3d64c42f6a..d9324297a7 100644
--- a/frappe/public/js/frappe/list/list_sidebar_group_by.js
+++ b/frappe/public/js/frappe/list/list_sidebar_group_by.js
@@ -103,7 +103,11 @@ frappe.views.ListGroupBy = class ListGroupBy {
this.render_dropdown_items(field_count_list, fieldtype, dropdown);
frappe.utils.setup_search(dropdown, '.group-by-item', '.group-by-value', 'data-name');
} else {
- dropdown.find('.group-by-loading').html(`${__("No filters found")}`);
+ dropdown.html(
+ `
+ ${__("No filters found")}
+
`
+ );
}
});
});
diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js
index dd9362d664..f2cba7c038 100644
--- a/frappe/public/js/frappe/list/list_view.js
+++ b/frappe/public/js/frappe/list/list_view.js
@@ -1,4 +1,5 @@
import BulkOperations from "./bulk_operations";
+import ListSettings from "./list_settings";
frappe.provide('frappe.views');
@@ -231,6 +232,20 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}
+ refresh_columns(meta, list_view_settings) {
+ this.meta = meta;
+ this.list_view_settings = list_view_settings;
+
+ this.setup_columns();
+ this.refresh(true);
+ }
+
+ refresh(refresh_header=false) {
+ super.refresh().then(() => {
+ this.render_header(refresh_header);
+ });
+ }
+
setup_freeze_area() {
this.$freeze =
$(`${__('Loading')}...
`)
@@ -287,19 +302,49 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}))
);
- // limit max to 8 columns
+ if (this.list_view_settings.fields) {
+ this.columns = this.reorder_listview_fields();
+ }
+
+ // limit max to 8 columns if no total_fields is set in List View Settings
// Screen with low density no of columns 4
// Screen with medium density no of columns 6
// Screen with high density no of columns 8
- let column_count = 6;
+ let total_fields = 6;
- if (window.innerWidth <= 1200) {
- column_count = 4;
- } else if (window.innerWidth > 1440) {
- column_count = 8;
+ if (window.innerWidth <= 1366) {
+ total_fields = 4;
+ } else if (window.innerWidth >= 1920) {
+ total_fields = 8;
}
- this.columns = this.columns.slice(0, column_count);
+ this.columns = this.columns.slice(0, this.list_view_settings.total_fields || total_fields);
+ }
+
+ reorder_listview_fields() {
+ let fields_order = [];
+ let fields = JSON.parse(this.list_view_settings.fields);
+
+ //title_field is fixed
+ fields_order.push(this.columns[0]);
+ this.columns.splice(0, 1);
+
+ for (let fld in fields) {
+ for (let col in this.columns) {
+ let field = fields[fld];
+ let column = this.columns[col];
+
+ if (column.type == "Status" && field.fieldname == "status_field") {
+ fields_order.push(column);
+ break;
+ } else if (column.type == "Field" && field.fieldname === column.df.fieldname) {
+ fields_order.push(column);
+ break;
+ }
+ }
+ }
+
+ return fields_order;
}
get_documentation_link() {
@@ -386,7 +431,11 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}
}
- render_header() {
+ render_header(refresh_header=false) {
+ if (refresh_header) {
+ this.$result.find('.list-row-head').remove();
+ }
+
if (this.$result.find('.list-row-head').length === 0) {
// append header once
this.$result.prepend(this.get_header_html());
@@ -1284,18 +1333,12 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}
show_list_settings() {
- frappe.model.with_doctype("List View Setting", () => {
- let d = new frappe.ui.Dialog({
- title: __("Settings"),
- fields: frappe.get_meta("List View Setting").fields
- });
- d.set_values(this.list_view_settings);
- d.show();
- d.set_primary_action(__('Save'), () => {
- let values = d.get_values();
- frappe.call("frappe.desk.listview.set_list_settings", {doctype: this.doctype, values: values});
- Object.assign(this.list_view_settings, values);
- d.hide();
+ frappe.model.with_doctype(this.doctype, () => {
+ new ListSettings({
+ listview: this,
+ doctype: this.doctype,
+ settings: this.list_view_settings,
+ meta: frappe.get_meta(this.doctype)
});
});
}
diff --git a/frappe/public/js/frappe/model/meta.js b/frappe/public/js/frappe/model/meta.js
index b7ad52838c..c2fd6b1ae6 100644
--- a/frappe/public/js/frappe/model/meta.js
+++ b/frappe/public/js/frappe/model/meta.js
@@ -161,8 +161,7 @@ $.extend(frappe.meta, {
if(!out) {
// eslint-disable-next-line
- console.log(__('Warning: Unable to find {0} in any table related to {1}', [
- key, __(doctype)]));
+ console.log(__('Warning: Unable to find {0} in any table related to {1}', [key, __(doctype)]));
}
}
return out;
@@ -266,5 +265,5 @@ $.extend(frappe.meta, {
precision = cint(frappe.defaults.get_default("float_precision")) || 3;
}
return precision;
- },
+ }
});
diff --git a/frappe/public/js/frappe/recorder/RecorderDetail.vue b/frappe/public/js/frappe/recorder/RecorderDetail.vue
index 68269ad0f4..53b3c8720b 100644
--- a/frappe/public/js/frappe/recorder/RecorderDetail.vue
+++ b/frappe/public/js/frappe/recorder/RecorderDetail.vue
@@ -114,8 +114,8 @@ export default {
{label: "Time", slug: "time", sortable: true},
],
query: {
- sort: "time",
- order: "asc",
+ sort: "duration",
+ order: "desc",
filters: {},
pagination: {
limit: 20,
diff --git a/frappe/public/js/frappe/recorder/RequestDetail.vue b/frappe/public/js/frappe/recorder/RequestDetail.vue
index 60795076ec..ac349d7937 100644
--- a/frappe/public/js/frappe/recorder/RequestDetail.vue
+++ b/frappe/public/js/frappe/recorder/RequestDetail.vue
@@ -79,7 +79,7 @@
-