diff --git a/.github/frappe_linter/translation.py b/.github/frappe_linter/translation.py new file mode 100644 index 0000000000..bb81e848f1 --- /dev/null +++ b/.github/frappe_linter/translation.py @@ -0,0 +1,28 @@ +import re +import sys + +errors_encounter = 0 +pattern = re.compile(r"_\(([\"']{,3})(?P((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P((?!\11).)*)\11)*)*\)") +start_pattern = re.compile(r"_{1,2}\([\"']{1,3}") + +# skip first argument +files = sys.argv[1:] +for _file in files: + if not _file.endswith(('.py', '.js')): + continue + with open(_file, 'r') as f: + print(f'Checking: {_file}') + for num, line in enumerate(f, 1): + all_matches = start_pattern.finditer(line) + if all_matches: + for match in all_matches: + verify = pattern.search(line) + if not verify: + errors_encounter += 1 + print(f'A syntax error has been discovered at line number: {num}') + print(f'Syntax error occurred with: {line}') +if errors_encounter > 0: + print('You can visit "https://frappe.io/docs/user/en/translations" to resolve this error.') + assert 1+1 == 3 +else: + print('Good To Go!') diff --git a/.github/workflows/translation_linter.yml b/.github/workflows/translation_linter.yml new file mode 100644 index 0000000000..ca7b152921 --- /dev/null +++ b/.github/workflows/translation_linter.yml @@ -0,0 +1,22 @@ +name: Frappe Linter +on: + pull_request: + branches: + - develop + - version-12-hotfix + - version-11-hotfix +jobs: + check_translation: + name: Translation Syntax Check + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + - name: Setup python3 + uses: actions/setup-python@v1 + with: + python-version: 3.6 + - name: Validating Translation Syntax + run: | + git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q + files=$(git diff --name-only $GITHUB_BASE_REF) + python $GITHUB_WORKSPACE/.github/frappe_linter/translation.py $files \ No newline at end of file diff --git a/.mergify.yml b/.mergify.yml index d810898eee..b145834cc4 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -1,7 +1,7 @@ pull_request_rules: - name: Automatic merge on CI success and review conditions: - - status-success=Codacy/PR Quality Review + - status-success=Sider - status-success=Semantic Pull Request - status-success=Travis CI - Pull Request - status-success=security/snyk - package.json (frappe) @@ -14,7 +14,7 @@ pull_request_rules: method: merge - name: Automatic squash on CI success and review conditions: - - status-success=Codacy/PR Quality Review + - status-success=Sider - status-success=Semantic Pull Request - status-success=Travis CI - Pull Request - status-success=security/snyk - package.json (frappe) diff --git a/cypress.json b/cypress.json index 7d853271b9..ae0c45c3ae 100644 --- a/cypress.json +++ b/cypress.json @@ -1,5 +1,7 @@ { "baseUrl": "http://test_site_ui:8000", "projectId": "92odwv", - "adminPassword": "admin" + "adminPassword": "admin", + "defaultCommandTimeout": 10000, + "pageLoadTimeout": 15000 } diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index 0c5b5f94b4..78f452db21 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -15,7 +15,8 @@ global_cache_keys = ("app_hooks", "installed_apps", "app_modules", "module_app", "system_settings", 'scheduler_events', 'time_zone', 'webhooks', 'active_domains', 'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version', - 'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts') + 'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts', + 'sitemap_routes') user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang", "defaults", "user_permissions", "home_page", "linked_with", diff --git a/frappe/commands/site.py b/frappe/commands/site.py index fd43cc8cf3..2b4923281c 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -1,13 +1,23 @@ -from __future__ import unicode_literals, absolute_import, print_function +# imports - standard imports +import atexit +import compileall +import hashlib +import os +import re +import shutil +import sys + +# imports - third party imports import click -import hashlib, os, sys, compileall, re + +# imports - module imports import frappe from frappe import _ -from frappe.commands import pass_context, get_site +from frappe.commands import get_site, pass_context from frappe.commands.scheduler import _is_scheduler_enabled from frappe.installer import update_site_config -from frappe.utils import touch_file, get_site_path -from six import text_type +from frappe.utils import get_site_path, touch_file + @click.command('new-site') @click.argument('site') @@ -68,32 +78,33 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N make_site_dirs() - installing = None - try: - installing = touch_file(get_site_path('locks', 'installing.lock')) + installing = touch_file(get_site_path('locks', 'installing.lock')) + atexit.register(_new_site_cleanup, site, mariadb_root_username, mariadb_root_password) - install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, - db_name=db_name, admin_password=admin_password, verbose=verbose, - source_sql=source_sql, force=force, reinstall=reinstall, db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port, no_mariadb_socket=no_mariadb_socket) + install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, db_name=db_name, + admin_password=admin_password, verbose=verbose, source_sql=source_sql, force=force, reinstall=reinstall, + db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port, no_mariadb_socket=no_mariadb_socket) + apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or []) + for app in apps_to_install: + _install_app(app, verbose=verbose, set_as_patched=not source_sql) - apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or []) - for app in apps_to_install: - _install_app(app, verbose=verbose, set_as_patched=not source_sql) + os.remove(installing) - frappe.utils.scheduler.toggle_scheduler(enable_scheduler) - frappe.db.commit() + frappe.utils.scheduler.toggle_scheduler(enable_scheduler) + frappe.db.commit() - scheduler_status = "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled" - print("*** Scheduler is", scheduler_status, "***") + scheduler_status = "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled" + print("*** Scheduler is", scheduler_status, "***") - except frappe.exceptions.ImproperDBConfigurationError: - _drop_site(site, mariadb_root_username, mariadb_root_password, force=True) +def _new_site_cleanup(site, mariadb_root_username, mariadb_root_password): + installing = get_site_path('locks', 'installing.lock') - finally: - if installing and os.path.exists(installing): - os.remove(installing) + if installing and os.path.exists(installing): + if mariadb_root_password: + _drop_site(site, mariadb_root_username, mariadb_root_password, force=True) + shutil.rmtree(site) - frappe.destroy() + frappe.destroy() @click.command('restore') @click.argument('sql-file-path') @@ -317,10 +328,18 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non "Backup" from frappe.utils.backups import scheduled_backup verbose = context.verbose + exit_code = 0 for site in context.sites: - frappe.init(site=site) - frappe.connect() - odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True) + try: + frappe.init(site=site) + frappe.connect() + odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True) + except Exception as e: + if verbose: + print("Backup failed for {0}. Database or site_config.json may be corrupted".format(site)) + exit_code = 1 + continue + if verbose: from frappe.utils import now print("database backup taken -", odb.backup_path_db, "- on", now()) @@ -329,6 +348,7 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non print("private files backup taken -", odb.backup_path_private_files, "- on", now()) frappe.destroy() + sys.exit(exit_code) @click.command('remove-from-installed-apps') @click.argument('app') diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index f970f51419..f7c9cbe28a 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -206,7 +206,7 @@ class DocType(Document): if d.fieldtype: if (not getattr(d, "fieldname", None)): if d.label: - d.fieldname = d.label.strip().lower().replace(' ','_') + d.fieldname = d.label.strip().lower().replace(' ','_').strip('?') if d.fieldname in restricted: d.fieldname = d.fieldname + '1' if d.fieldtype=='Section Break': @@ -914,7 +914,7 @@ def validate_fields(meta): if not d.permlevel: d.permlevel = 0 if d.fieldtype not in table_fields: d.allow_bulk_edit = 0 if not d.fieldname: - d.fieldname = d.fieldname.lower() + d.fieldname = d.fieldname.lower().strip('?') check_illegal_characters(d.fieldname) check_invalid_fieldnames(meta.get("name"), d.fieldname) diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 7f763ea9fc..8741101976 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -608,8 +608,7 @@ def get_local_image(file_url): try: image = Image.open(file_path) except IOError: - frappe.msgprint(_("Unable to read file format for {0}").format(file_url)) - raise + frappe.msgprint(_("Unable to read file format for {0}").format(file_url), raise_exception=True) content = None diff --git a/frappe/core/doctype/language/language.json b/frappe/core/doctype/language/language.json index 099b383980..eed29883c1 100644 --- a/frappe/core/doctype/language/language.json +++ b/frappe/core/doctype/language/language.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_rename": 1, "autoname": "field:language_code", "creation": "2014-08-22 16:12:17.249590", @@ -41,7 +42,9 @@ } ], "icon": "fa fa-globe", - "modified": "2019-07-19 16:32:12.652550", + "in_create": 1, + "links": [], + "modified": "2020-04-16 22:11:33.066852", "modified_by": "Administrator", "module": "Core", "name": "Language", diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json index e2ec921679..2a9c1a4573 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json @@ -13,6 +13,7 @@ "field_order": [ "stopped", "method", + "server_script", "frequency", "cron_format", "last_execution", @@ -63,6 +64,14 @@ "options": "All\nHourly\nHourly Long\nDaily\nDaily Long\nWeekly\nWeekly Long\nMonthly\nMonthly Long\nCron\nYearly\nAnnual", "read_only": 1, "reqd": 1 + }, + { + "fieldname": "server_script", + "fieldtype": "Link", + "label": "Server Script", + "options": "Server Script", + "read_only": 1, + "search_index": 1 } ], "in_create": 1, @@ -72,7 +81,7 @@ "link_fieldname": "scheduled_job_type" } ], - "modified": "2019-12-09 11:10:21.259929", + "modified": "2020-04-05 17:27:33.480562", "modified_by": "Administrator", "module": "Core", "name": "Scheduled Job Type", 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 3cd994ebfa..c179054550 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -70,7 +70,12 @@ class ScheduledJobType(Document): self.scheduler_log = None try: self.log_status('Start') - frappe.get_attr(self.method)() + if self.server_script: + script_name = frappe.db.get_value("Server Script", self.server_script) + if script_name: + frappe.get_doc('Server Script', script_name).execute_scheduled_method() + else: + frappe.get_attr(self.method)() frappe.db.commit() self.log_status('Complete') except Exception: diff --git a/frappe/core/doctype/server_script/server_script.js b/frappe/core/doctype/server_script/server_script.js index eea8558456..d7f4c3e536 100644 --- a/frappe/core/doctype/server_script/server_script.js +++ b/frappe/core/doctype/server_script/server_script.js @@ -2,7 +2,45 @@ // For license information, please see license.txt frappe.ui.form.on('Server Script', { - // refresh: function(frm) { + refresh: function(frm) { + if(frm.doc.script_type === 'Scheduler Event' && !frm.doc.disabled){ + frm.add_custom_button('Schedule Script', function() { + var d = new frappe.ui.Dialog({ + title: "Schedule Script Execution", + fields: [ + { + fieldname: "event_type", + label: __('Select Event Type'), + fieldtype: "Select", + options: "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long" + }, + ], + primary_action_label: __('Schedule Script'), + primary_action: () => { + d.get_primary_btn().attr('disabled', true); + var data = d.get_values(); + d.hide(); + if(data) { + frm.events.schedule_script(frm, data); + } + + } + }); + + d.show(); + + }); + } + }, + + schedule_script(frm, data){ + frm.call({ + method: "frappe.core.doctype.server_script.server_script.setup_scheduler_events", + args: { + 'script_name': frm.doc.name, + 'frequency': data.event_type + } + }) + } - // } }); diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index 36c297cc26..bef3dfc60c 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -22,7 +22,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Script Type", - "options": "DocType Event\nAPI", + "options": "DocType Event\nScheduler Event\nAPI", "reqd": 1 }, { @@ -75,7 +75,7 @@ } ], "links": [], - "modified": "2019-12-17 12:55:07.389775", + "modified": "2020-04-06 11:24:38.161555", "modified_by": "Administrator", "module": "Core", "name": "Server Script", diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index e2c6d3b7b0..9522b77b4b 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -7,6 +7,8 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document from frappe.utils.safe_exec import safe_exec +from frappe import _ + class ServerScript(Document): @staticmethod @@ -31,3 +33,39 @@ class ServerScript(Document): # execute event safe_exec(self.script, None, dict(doc = doc)) + def execute_scheduled_method(self): + if self.script_type == 'Scheduler Event': + safe_exec(self.script) + else: + # wrong report type! + raise frappe.DoesNotExistError + +@frappe.whitelist() +def setup_scheduler_events(script_name, frequency): + method = frappe.scrub(script_name) + '_' + frequency.lower() + scheduled_script = frappe.db.get_value('Scheduled Job Type', + dict(method=method)) + + if not scheduled_script: + doc = frappe.get_doc(dict( + doctype = 'Scheduled Job Type', + method = method, + frequency = frequency, + server_script = script_name + )) + + doc.insert() + + frappe.msgprint(_('Enabled scheduled execution for script {0}').format(script_name)) + + else: + doc = frappe.get_doc('Scheduled Job Type', scheduled_script) + doc.update(dict( + doctype = 'Scheduled Job Type', + method = method, + frequency = frequency, + server_script = script_name + )) + doc.save() + + frappe.msgprint(_('Scheduled execution for script {0} has updated').format(script_name)) diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index 2e1a5ae8bb..e03504f30b 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -66,6 +66,7 @@ def get_server_script_map(): script_map.setdefault(script.reference_doctype, {}).setdefault(script.doctype_event, []).append(script.name) else: script_map.setdefault('_api', {})[script.api_method] = script.name + frappe.cache().set_value('server_script_map', script_map) - return script_map + return script_map \ No newline at end of file diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 68848d26f6..7d081953dd 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -208,9 +208,11 @@ class CustomizeForm(Document): self.validate_fieldtype_change(df, meta_df[0].get(property), df.get(property)) elif property == "allow_on_submit" and df.get(property): - frappe.msgprint(_("Row {0}: Not allowed to enable Allow on Submit for standard fields")\ - .format(df.idx)) - continue + if not frappe.db.get_value("DocField", + {"parent": self.doc_type, "fieldname": df.fieldname}, "allow_on_submit"): + frappe.msgprint(_("Row {0}: Not allowed to enable Allow on Submit for standard fields")\ + .format(df.idx)) + continue elif property == "reqd" and \ ((frappe.db.get_value("DocField", @@ -369,7 +371,12 @@ class CustomizeForm(Document): for allowed_changes in allowed_fieldtype_change: if (old_value in allowed_changes and new_value in allowed_changes): allowed = True - if frappe.db.type_map.get(old_value)[1] > frappe.db.type_map.get(new_value)[1]: + old_value_length = cint(frappe.db.type_map.get(old_value)[1]) + new_value_length = cint(frappe.db.type_map.get(new_value)[1]) + + # Ignore fieldtype check validation if new field type has unspecified maxlength + # Changes like DATA to TEXT, where new_value_lenth equals 0 will not be validated + if new_value_length and (old_value_length > new_value_length): self.check_length_for_fieldtypes.append({'df': df, 'old_value': old_value}) self.validate_fieldtype_length() else: @@ -381,7 +388,7 @@ class CustomizeForm(Document): def validate_fieldtype_length(self): for field in self.check_length_for_fieldtypes: df = field.get('df') - max_length = frappe.db.type_map.get(df.fieldtype)[1] + max_length = cint(frappe.db.type_map.get(df.fieldtype)[1]) fieldname = df.fieldname docs = frappe.db.sql(''' SELECT name, {fieldname}, LENGTH({fieldname}) AS len diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 13b2b61bef..9ab1ef7799 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -506,9 +506,13 @@ class BaseDocument(object): fetch_from_fieldname = _df.fetch_from.split('.')[-1] value = values[fetch_from_fieldname] if _df.fieldtype == 'Small Text' or _df.fieldtype == 'Text' or _df.fieldtype == 'Data': - fetch_from_df = frappe.get_meta(doctype).get_field(fetch_from_fieldname) - fetch_from_ft = fetch_from_df.get('fieldtype') + if fetch_from_fieldname in default_fields: + from frappe.model.meta import get_default_df + fetch_from_df = get_default_df(fetch_from_fieldname) + else: + fetch_from_df = frappe.get_meta(doctype).get_field(fetch_from_fieldname) + fetch_from_ft = fetch_from_df.get('fieldtype') if fetch_from_ft == 'Text Editor' and value: value = unescape_html(strip_html(value)) setattr(self, _df.fieldname, value) diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index d77898020d..c0d2c4eef9 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -210,7 +210,7 @@ def check_permission_and_not_submitted(doc): # check if submitted if doc.docstatus == 1: - frappe.msgprint(_("{0} {1}: Submitted Record cannot be deleted.").format(_(doc.doctype), doc.name), + frappe.msgprint(_("{0} {1}: Submitted Record cannot be deleted. You must {2} Cancel {3} it first.").format(_(doc.doctype), doc.name, "", ""), raise_exception=True) def check_if_doc_is_linked(doc, method="Delete"): diff --git a/frappe/patches.txt b/frappe/patches.txt index 0e02423639..cbda8cf677 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -272,3 +272,4 @@ execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Account') execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Settings') frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders() +frappe.patches.v13_0.website_theme_custom_scss diff --git a/frappe/patches/v13_0/__init__.py b/frappe/patches/v13_0/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/patches/v13_0/website_theme_custom_scss.py b/frappe/patches/v13_0/website_theme_custom_scss.py new file mode 100644 index 0000000000..9d5a78643e --- /dev/null +++ b/frappe/patches/v13_0/website_theme_custom_scss.py @@ -0,0 +1,10 @@ +import frappe + +def execute(): + frappe.reload_doctype('Website Theme') + for theme in frappe.get_all('Website Theme'): + doc = frappe.get_doc('Website Theme', theme.name) + if not doc.custom_scss and doc.theme_scss: + # move old theme to new theme + doc.custom_scss = doc.theme_scss + doc.save() \ No newline at end of file diff --git a/frappe/public/js/frappe/form/controls/text_editor.js b/frappe/public/js/frappe/form/controls/text_editor.js index b35c92c1ae..4e18b081cc 100644 --- a/frappe/public/js/frappe/form/controls/text_editor.js +++ b/frappe/public/js/frappe/form/controls/text_editor.js @@ -147,7 +147,7 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ [{ 'color': [] }, { 'background': [] }], ['blockquote', 'code-block'], ['link', 'image'], - [{ 'list': 'ordered' }, { 'list': 'bullet' }], + [{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'list': 'check' }], [{ 'align': [] }], [{ 'indent': '-1'}, { 'indent': '+1' }], [{'table': [ diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index c4c739dbcc..3b6ccd9a5c 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -125,8 +125,9 @@ frappe.ui.form.Dashboard = Class.extend({ }, format_percent: function(title, percent) { - var width = cint(percent) < 1 ? 1 : cint(percent); - var progress_class = "progress-bar-success"; + const percentage = cint(percent); + const width = percentage < 0 ? 100 : percentage; + const progress_class = percentage < 0 ? "progress-bar-danger" : "progress-bar-success"; return [{ title: title, diff --git a/frappe/public/js/frappe/form/save.js b/frappe/public/js/frappe/form/save.js index d40b3ed341..7efbbe2d3d 100644 --- a/frappe/public/js/frappe/form/save.js +++ b/frappe/public/js/frappe/form/save.js @@ -21,7 +21,7 @@ frappe.ui.form.save = function (frm, action, callback, btn) { remove_empty_rows(); $(frm.wrapper).addClass('validated-form'); - if (frm.is_dirty() && check_mandatory()) { + if ((action !== 'Save' || frm.is_dirty()) && check_mandatory()) { _call({ method: "frappe.desk.form.save.savedocs", args: { doc: frm.doc, action: action }, diff --git a/frappe/public/js/frappe/ui/group_by/group_by.js b/frappe/public/js/frappe/ui/group_by/group_by.js index 6936f25c18..dc81bbdf20 100644 --- a/frappe/public/js/frappe/ui/group_by/group_by.js +++ b/frappe/public/js/frappe/ui/group_by/group_by.js @@ -92,7 +92,6 @@ frappe.ui.GroupBy = class { } apply_settings(settings) { - if (!settings.group_by.startsWith('`tab')) { settings.group_by = '`tab' + this.doctype + '`.`' + settings.group_by + '`'; } diff --git a/frappe/public/js/frappe/utils/pretty_date.js b/frappe/public/js/frappe/utils/pretty_date.js index 1ad02e675c..060ae73a98 100644 --- a/frappe/public/js/frappe/utils/pretty_date.js +++ b/frappe/public/js/frappe/utils/pretty_date.js @@ -1,64 +1,71 @@ -// moment strings for translation +function prettyDate(date, mini) { + if (!date) return ''; -function prettyDate(time, mini) { - if (!time) { - time = new Date(); - } - if ('moment' in window) { // use frappe.ImportError ;) - let ret; - if (frappe.sys_defaults && frappe.sys_defaults.time_zone) { - ret = moment.tz(time, frappe.sys_defaults.time_zone).locale(frappe.boot.lang).fromNow(mini); - } else { - ret = moment(time).locale(frappe.boot.lang).fromNow(mini); - } - if (mini) { - if (ret === moment().locale(frappe.boot.lang).fromNow(mini)) { - ret = __("now"); - } else { - var parts = ret.split(" "); - if (parts.length > 1) { - if (parts[0] === "a" || parts[0] === "an") { - parts[0] = 1; - } - if (parts[1].substr(0, 2) === "mo") { - ret = parts[0] + " M"; - } else { - ret = parts[0] + " " + parts[1].substr(0, 1); - } - } + if (typeof (date) == "string") + date = new Date((date || "").replace(/-/g, "/").replace(/[TZ]/g, " ").replace(/\.[0-9]*/, "")); + + let diff = (((new Date()).getTime() - date.getTime()) / 1000); + let day_diff = Math.floor(diff / 86400); + + if (isNaN(day_diff) || day_diff < 0) return ''; + + if (mini) { + // Return short format of time difference + if (day_diff == 0) { + if (diff < 60) { + return __("Now"); + } else if (diff < 3600) { + return __("{0} m", [Math.floor(diff / 60)]); + } else if (diff < 86400) { + return __("{0} h", [Math.floor(diff / 3600)]); + } + } else { + if (day_diff < 7) { + return __("{0} D", [day_diff]); + } else if (day_diff < 31) { + return __("{0} W", [Math.ceil(day_diff / 7)]); + } else if (day_diff < 365) { + return __("{0} M", [Math.ceil(day_diff / 30)]); + } else { + return __("{0} Y", [Math.ceil(day_diff / 365)]); } - ret = ret.substr(0, 5); } - return ret; } else { - if (!time) return ''; - var date = time; - if (typeof (time) == "string") - date = new Date((time || "").replace(/-/g, "/").replace(/[TZ]/g, " ").replace(/\.[0-9]*/, "")); - - var diff = (((new Date()).getTime() - date.getTime()) / 1000), - day_diff = Math.floor(diff / 86400); - - if (isNaN(day_diff) || day_diff < 0) - return ''; - - var when = day_diff == 0 && ( - diff < 60 && __("just now") || - diff < 120 && __("1 minute ago") || - diff < 3600 && __("{0} minutes ago", [Math.floor(diff / 60)]) || - diff < 7200 && __("1 hour ago") || - diff < 86400 && ("{0} hours ago", [Math.floor(diff / 3600)])) || - day_diff == 1 && __("Yesterday") || - day_diff < 7 && __("{0} days ago", day_diff) || - day_diff < 31 && __("{0} weeks ago", [Math.ceil(day_diff / 7)]) || - day_diff < 365 && __("{0} months ago", [Math.ceil(day_diff / 30)]) || - __("> {0} year(s) ago", [Math.floor(day_diff / 365)]); - - return when; + // Return long format of time difference + if (day_diff == 0) { + if (diff < 60) { + return __("Just now"); + } else if (diff < 120) { + return __("1 minute ago"); + } else if (diff < 3600) { + return __("{0} minutes ago", [Math.floor(diff / 60)]); + } else if (diff < 7200) { + return __("1 hour ago"); + } else if (diff < 86400) { + return __("{0} hours ago", [Math.floor(diff / 3600)]); + } + } else { + if (day_diff == 1) { + return __("Yesterday"); + } else if (day_diff < 7) { + return __("{0} days ago", [day_diff]); + } else if (day_diff < 14) { + return __("1 week ago"); + } else if (day_diff < 31) { + return __("{0} weeks ago", [Math.ceil(day_diff / 7)]); + } else if (day_diff < 62) { + return __("1 month ago"); + } else if (day_diff < 365) { + return __("{0} months ago", [Math.ceil(day_diff / 30)]); + } else if (day_diff < 730) { + return __("1 year ago"); + } else { + return __("{0} years ago", [Math.ceil(day_diff / 365)]); + } + } } } - frappe.provide("frappe.datetime"); window.comment_when = function(datetime, mini) { var timestamp = frappe.datetime.str_to_user ? diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index a2b03f180e..0f27e97178 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -675,7 +675,9 @@ Object.assign(frappe.utils, { return __(frappe.utils.to_title_case(route[0], true)); }, report_column_total: function(values, column, type) { - if (values.length > 0) { + if (column.column.disable_total) { + return ''; + } else if (values.length > 0) { if (column.column.fieldtype == "Percent" || type === "mean") { return values.reduce((a, b) => a + flt(b)) / values.length; } else if (column.column.fieldtype == "Int") { diff --git a/frappe/public/js/frappe/views/desktop/desktop.js b/frappe/public/js/frappe/views/desktop/desktop.js index 12d33affa7..9f0df6e172 100644 --- a/frappe/public/js/frappe/views/desktop/desktop.js +++ b/frappe/public/js/frappe/views/desktop/desktop.js @@ -203,8 +203,8 @@ class DesktopPage { this.allow_customization && this.make_customization_link(); let create_shortcuts_and_cards = () => { - this.data.shortcuts.items.length && this.make_shortcuts(); - this.data.cards.items.length && this.make_cards(); + this.make_shortcuts(); + this.make_cards(); if (this.allow_customization) { // Move the widget group up to align with labels if customization is allowed @@ -212,7 +212,7 @@ class DesktopPage { } }; - if (!this.sections["onboarding"] && this.data.charts.items.length) { + if (!this.sections["onboarding"]) { this.make_charts().then(() => { create_shortcuts_and_cards(); }); diff --git a/frappe/public/js/frappe/web_form/webform_script.js b/frappe/public/js/frappe/web_form/webform_script.js index 7bf7162101..53d9701774 100644 --- a/frappe/public/js/frappe/web_form/webform_script.js +++ b/frappe/public/js/frappe/web_form/webform_script.js @@ -2,13 +2,13 @@ import WebFormList from './web_form_list' import WebForm from './web_form' frappe.ready(function() { + let query_params = frappe.utils.get_query_params(); let wrapper = $(".web-form-wrapper"); - let is_list = parseInt(wrapper.data('is-list')); + let is_list = parseInt(wrapper.data('is-list')) || query_params.is_list; let webform_doctype = wrapper.data('web-form-doctype'); let webform_name = wrapper.data('web-form'); let login_required = parseInt(wrapper.data('login-required')); let allow_delete = parseInt(wrapper.data('allow-delete')); - let query_params = frappe.utils.get_query_params(); let doc_name = query_params.name || ''; let is_new = query_params.new; @@ -38,7 +38,7 @@ frappe.ready(function() { settings: { allow_delete } - }) + }); } function show_form() { @@ -113,8 +113,9 @@ frappe.ready(function() { df.only_select = true; } if (["Attach", "Attach Image"].includes(df.fieldtype)) { - if (!df.options) + if (typeof df.options !== "object") { df.options = {}; + } df.options.disable_file_browser = true; } }); diff --git a/frappe/public/less/common.less b/frappe/public/less/common.less index b0aca5cb47..c2de48d553 100644 --- a/frappe/public/less/common.less +++ b/frappe/public/less/common.less @@ -32,6 +32,10 @@ details > summary { cursor: pointer; } +details > summary:focus { + outline: none; +} + .text-color { color: @text-color !important; } diff --git a/frappe/public/scss/website.scss b/frappe/public/scss/website.scss index a297054ee2..6f82e25ee0 100644 --- a/frappe/public/scss/website.scss +++ b/frappe/public/scss/website.scss @@ -4,8 +4,23 @@ @import "multilevel-dropdown"; @import "website-image"; +html { + height: 100%; +} + body { + min-height: 100%; + display: flex; + flex-direction: column; font-size: 16px; + + > div { + flex: 1 0 auto; + } +} + +footer { + flex-shrink: 0; } .navbar.bg-dark { diff --git a/frappe/templates/base.html b/frappe/templates/base.html index 1c5f286442..2a241c4843 100644 --- a/frappe/templates/base.html +++ b/frappe/templates/base.html @@ -62,7 +62,11 @@ {%- endblock -%} {%- block navbar -%} - {% include "templates/includes/navbar/navbar.html" %} + {%- if navbar_content -%} + {{ navbar_content }} + {%- else -%} + {% include "templates/includes/navbar/navbar.html" %} + {%- endif -%} {%- endblock -%} {% block content %} @@ -70,7 +74,11 @@ {% endblock %} {%- block footer -%} - {% include "templates/includes/footer/footer.html" %} + {%- if footer_content -%} + {{ footer_content }} + {%- else -%} + {% include "templates/includes/footer/footer.html" %} + {%- endif -%} {%- endblock -%} {% block base_scripts %} diff --git a/frappe/templates/web.html b/frappe/templates/web.html index e61672124a..d2d38a6320 100644 --- a/frappe/templates/web.html +++ b/frappe/templates/web.html @@ -13,7 +13,7 @@ {% block page_container %} -
+