diff --git a/.travis.yml b/.travis.yml
index 9fab56188b..a1568c9118 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -47,23 +47,27 @@ matrix:
script: bench --site test_site run-ui-tests frappe --headless
before_install:
- # do we really want to run travis?
+ # do we really want to run travis? check which files are changed and if git doesnt face any fatal errors
- |
- ONLY_DOCS_CHANGES=$(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '\.(md|png|jpg|jpeg)$|^.github|LICENSE' ; echo $?)
- ONLY_JS_CHANGES=$(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '\.js$' ; echo $?)
- ONLY_PY_CHANGES=$(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '\.py$' ; echo $?)
+ FILES_CHANGED=$( git diff --name-only $TRAVIS_COMMIT_RANGE 2>&1 )
- 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;
+ 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
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/integration/control_duration.js b/cypress/integration/control_duration.js
index f304abd3d9..edad759216 100644
--- a/cypress/integration/control_duration.js
+++ b/cypress/integration/control_duration.js
@@ -4,14 +4,14 @@ context('Control Duration', () => {
cy.visit('/desk#workspace/Website');
});
- function get_dialog_with_duration(show_days=1, show_seconds=1) {
+ function get_dialog_with_duration(hide_days=0, hide_seconds=0) {
return cy.dialog({
title: 'Duration',
fields: [{
'fieldname': 'duration',
'fieldtype': 'Duration',
- 'show_seconds': show_days,
- 'show_days': show_seconds
+ 'hide_days': hide_days,
+ 'hide_seconds': hide_seconds
}]
});
}
@@ -37,7 +37,7 @@ context('Control Duration', () => {
});
it('should hide days or seconds according to duration options', () => {
- get_dialog_with_duration(0, 0).as('dialog');
+ 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');
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 411ede62fa..ac70c44345 100644
--- a/cypress/integration/relative_filters.js
+++ b/cypress/integration/relative_time_filters.js
@@ -9,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();
@@ -28,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 695a4d754b..8862ce3c61 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -85,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
@@ -297,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..42f4440547 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()
@@ -44,8 +48,7 @@ def get_site(context):
site = context.sites[0]
return site
except (IndexError, TypeError):
- print('Please specify --site sitename')
- sys.exit(1)
+ raise frappe.SiteNotSpecifiedError
def popen(command, *args, **kwargs):
output = kwargs.get('output', True)
diff --git a/frappe/commands/scheduler.py b/frappe/commands/scheduler.py
index 6f51c81211..511fac6e0d 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')
diff --git a/frappe/commands/site.py b/frappe/commands/site.py
index f0c4adb157..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
@@ -82,10 +83,6 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
installing = touch_file(get_site_path('locks', 'installing.lock'))
- if new_site:
- # run cleanup only if new-site is called
- atexit.register(_new_site_cleanup, site, mariadb_root_username, mariadb_root_password)
-
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, db_name=db_name,
admin_password=admin_password, verbose=verbose, source_sql=source_sql, force=force, reinstall=reinstall,
db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port, no_mariadb_socket=no_mariadb_socket)
@@ -101,18 +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):
- try:
- installing = get_site_path('locks', 'installing.lock')
- except AttributeError:
- installing = os.path.join(site, 'locks', 'installing.lock')
-
- if installing and os.path.exists(installing):
- if mariadb_root_password:
- _drop_site(site, mariadb_root_username, mariadb_root_password, force=True, no_backup=True)
- shutil.rmtree(site)
-
- frappe.destroy()
@click.command('restore')
@click.argument('sql-file-path')
@@ -130,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')
@@ -200,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
@@ -229,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')
@@ -260,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.*'))
@@ -271,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')
@@ -286,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')
@@ -302,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')
@@ -316,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
@@ -323,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')
@@ -336,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")
@@ -369,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')
@@ -384,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')
@@ -400,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')
@@ -430,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)
@@ -491,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")
@@ -536,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)
@@ -563,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')
@@ -571,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 20e4774add..232d485f36 100644
--- a/frappe/core/doctype/communication/communication.py
+++ b/frappe/core/doctype/communication/communication.py
@@ -444,24 +444,48 @@ def update_parent_document_on_communication(doc):
status_field = parent.meta.get_field("status")
if status_field:
- options = (status_field.options or '').splitlines()
+ 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":
+ 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)
- parent.run_method('notify_communication', 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 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)
+ 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/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json
index 83d3c18453..aab59a5a0a 100644
--- a/frappe/core/doctype/docfield/docfield.json
+++ b/frappe/core/doctype/docfield/docfield.json
@@ -13,8 +13,8 @@
"fieldname",
"precision",
"length",
- "show_days",
- "show_seconds",
+ "hide_days",
+ "hide_seconds",
"reqd",
"search_index",
"in_list_view",
@@ -453,18 +453,18 @@
"fieldtype": "Column Break"
},
{
- "default": "1",
- "depends_on": "eval:doc.fieldtype === \"Duration\";",
- "fieldname": "show_days",
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_days",
"fieldtype": "Check",
- "label": "Show Days"
+ "label": "Hide Days"
},
{
- "default": "1",
- "depends_on": "eval:doc.fieldtype === \"Duration\";",
- "fieldname": "show_seconds",
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_seconds",
"fieldtype": "Check",
- "label": "Show Seconds"
+ "label": "Hide Seconds"
},
{
"default": "0",
@@ -477,7 +477,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-05-15 09:06:25.224411",
+ "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 0f57ed0a5e..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:
@@ -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
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/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json
index 77490c8c43..6fa7b29161 100644
--- a/frappe/custom/doctype/custom_field/custom_field.json
+++ b/frappe/custom/doctype/custom_field/custom_field.json
@@ -16,8 +16,8 @@
"column_break_6",
"fieldtype",
"precision",
- "show_seconds",
- "show_days",
+ "hide_seconds",
+ "hide_days",
"options",
"fetch_from",
"fetch_if_empty",
@@ -383,22 +383,18 @@
"label": "In Preview"
},
{
- "default": "1",
- "depends_on": "eval:doc.fieldtype === \"Duration\";",
- "fieldname": "show_seconds",
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_seconds",
"fieldtype": "Check",
- "label": "Show Seconds",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Hide Seconds"
},
{
- "default": "1",
- "depends_on": "eval:doc.fieldtype === \"Duration\";",
- "fieldname": "show_days",
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_days",
"fieldtype": "Check",
- "label": "Show Days",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Hide Days"
},
{
"default": "0",
@@ -411,7 +407,7 @@
"icon": "fa fa-glass",
"idx": 1,
"links": [],
- "modified": "2020-05-15 23:43:00.123572",
+ "modified": "2020-02-06 23:43:00.123575",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",
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 f422c36e61..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,8 +11,8 @@
"label",
"fieldtype",
"fieldname",
- "show_seconds",
- "show_days",
+ "hide_seconds",
+ "hide_days",
"reqd",
"unique",
"in_list_view",
@@ -393,22 +393,18 @@
"label": "In Preview"
},
{
- "default": "1",
- "depends_on": "eval:doc.fieldtype === \"Duration\";",
- "fieldname": "show_seconds",
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_seconds",
"fieldtype": "Check",
- "label": "Show Seconds",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Hide Seconds"
},
{
- "default": "1",
- "depends_on": "eval:doc.fieldtype === \"Duration\";",
- "fieldname": "show_days",
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_days",
"fieldtype": "Check",
- "label": "Show Days",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Hide Days"
},
{
"default": "0",
@@ -421,7 +417,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-05-15 23:45:46.810869",
+ "modified": "2020-06-02 23:45:46.810868",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",
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/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/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/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/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/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:
\n
doc.status==\"Open\"
doc.due_date==nowdate()
doc.total > 40000\n
\n"
+ "options": "
Condition Examples:
\n
doc.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 fe8c1895d7..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
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 4b595b1abf..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 = {
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/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/meta.py b/frappe/model/meta.py
index 0c5ec75597..1cc3abba5b 100644
--- a/frappe/model/meta.py
+++ b/frappe/model/meta.py
@@ -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 0a7d368ee2..582b369343 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -278,6 +278,7 @@ 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
@@ -285,4 +286,6 @@ 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")
\ No newline at end of file
+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/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/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/duration.js b/frappe/public/js/frappe/form/controls/duration.js
index 58df8e15e6..e70afd6e65 100644
--- a/frappe/public/js/frappe/form/controls/duration.js
+++ b/frappe/public/js/frappe/form/controls/duration.js
@@ -13,10 +13,10 @@ frappe.ui.form.ControlDuration = frappe.ui.form.ControlData.extend({
`
);
this.$wrapper.append(this.$picker);
- this.build_numeric_input("days", !this.duration_options.show_days);
+ 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.show_seconds);
+ this.build_numeric_input("seconds", this.duration_options.hide_seconds);
this.set_duration_picker_value(this.value);
this.$picker.hide();
this.bind_events();
@@ -130,10 +130,10 @@ frappe.ui.form.ControlDuration = frappe.ui.form.ControlData.extend({
if (this.inputs) {
total_duration.minutes = parseInt(this.inputs.minutes.val());
total_duration.hours = parseInt(this.inputs.hours.val());
- if (this.duration_options.show_days) {
+ if (!this.duration_options.hide_days) {
total_duration.days = parseInt(this.inputs.days.val());
}
- if (this.duration_options.show_seconds) {
+ if (!this.duration_options.hide_seconds) {
total_duration.seconds = parseInt(this.inputs.seconds.val());
}
}
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 += `
+ ${__('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/socketio_client.js b/frappe/public/js/frappe/socketio_client.js
index 9983a35779..1411b6289d 100644
--- a/frappe/public/js/frappe/socketio_client.js
+++ b/frappe/public/js/frappe/socketio_client.js
@@ -287,7 +287,8 @@ frappe.socketio.SocketIOUploader = class SocketIOUploader {
}
function fallback_required() {
- return !frappe.boot.sysdefaults.use_socketio_to_upload_file || !frappe.socketio.socket.connected;
+ return !frappe.socketio.socket.connected
+ || !( !frappe.boot.sysdefaults || frappe.boot.sysdefaults.use_socketio_to_upload_file );
}
if (fallback_required()) {
diff --git a/frappe/public/js/frappe/ui/dialog.js b/frappe/public/js/frappe/ui/dialog.js
index 0f4332a91a..d77481f8b9 100644
--- a/frappe/public/js/frappe/ui/dialog.js
+++ b/frappe/public/js/frappe/ui/dialog.js
@@ -42,6 +42,8 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
this.body = this.$body.get(0);
this.$message = $('
{{ item.group_title }}
+ {{ render_sidebar_items(item.group_items) }} + + {%- else -%} + + {% if item.type != 'input' %} + {%- set item_route = item.route[1:] if item.route[0] == '/' else item.route -%} + + {{ _(item.title or item.label) }} + + {% else %} + + {% endif %} + + {%- endif -%} ++ {% for item in items -%} + {{ render_sidebar_item(item) }} + {%- endfor %} +
+{%- endif -%} +{% endmacro %} + +{% macro my_account() %} +{% if frappe.user != 'Guest' %} ++-
+ {{ _("My Account") }}
+
+
+{% endif %} +{% endmacro %} +- {% if sidebar_title %} --
- {{ sidebar_title }}
-
- {% endif %}
- {% for item in sidebar_items -%}
- -
- {% if item.type != 'input' %}
- {%- set item_route = item.route[1:] if item.route[0] == '/' else item.route -%}
-
- {{ _(item.title or item.label) }}
-
- {% else %}
-
- {% endif %}
-
- {%- endfor %}
- {% if frappe.user != 'Guest' %}
- -
- {{ _("My Account") }}
-
- {% endif %}
-
+ {{ render_sidebar_items(sidebar_items) }} + {{ my_account() }}{{ title }}
-- {{ blog_intro }} -
- - -{{ title }}
++ {{ blog_intro }} +
++
@@ -45,7 +64,7 @@ {% endif %} {% if not disable_comments %} -
{{ post.title }}
-{{ post.intro }}
-{{ post.title }}
+ {%- else -%} +{{ post.title }} + {%- endif -%} +
{{ post.intro }}
+