diff --git a/.github/frappe-framework-logo.png b/.github/frappe-framework-logo.png deleted file mode 100644 index 5049078a46..0000000000 Binary files a/.github/frappe-framework-logo.png and /dev/null differ diff --git a/.github/frappe-framework-logo.svg b/.github/frappe-framework-logo.svg new file mode 100644 index 0000000000..ba04ebf264 --- /dev/null +++ b/.github/frappe-framework-logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/.gitignore b/.gitignore index 7df673c1f1..900ae1c7b7 100644 --- a/.gitignore +++ b/.gitignore @@ -188,4 +188,7 @@ typings/ # cypress cypress/screenshots -cypress/videos \ No newline at end of file +cypress/videos + +# JetBrains IDEs +.idea/ diff --git a/.mergify.yml b/.mergify.yml index b145834cc4..582bbc2ee5 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -4,8 +4,7 @@ pull_request_rules: - status-success=Sider - status-success=Semantic Pull Request - status-success=Travis CI - Pull Request - - status-success=security/snyk - package.json (frappe) - - status-success=security/snyk - requirements.txt (frappe) + - status-success=security/snyk (frappe) - label!=don't-merge - label!=squash - "#approved-reviews-by>=1" @@ -17,8 +16,7 @@ pull_request_rules: - status-success=Sider - status-success=Semantic Pull Request - status-success=Travis CI - Pull Request - - status-success=security/snyk - package.json (frappe) - - status-success=security/snyk - requirements.txt (frappe) + - status-success=security/snyk (frappe) - label!=don't-merge - label=squash - "#approved-reviews-by>=1" diff --git a/README.md b/README.md index 7545249610..1f59376f48 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@
-

+
- frappe +

- a web framework with "batteries included" + a web framework with "batteries included"

it's pronounced - fra-pay diff --git a/frappe/__init__.py b/frappe/__init__.py index f35409fa48..45f40f7783 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -490,7 +490,8 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message message = content or message if as_markdown: - message = frappe.utils.md_to_html(message) + from frappe.utils import md_to_html + message = md_to_html(message) if not delayed: now = True @@ -1558,10 +1559,10 @@ def get_doctype_app(doctype): loggers = {} log_level = None -def logger(module=None, with_more_info=False): +def logger(module=None, with_more_info=False, allow_site=True, filter=None, max_size=100_000, file_count=20): '''Returns a python logger that uses StreamHandler''' from frappe.utils.logger import get_logger - return get_logger(module=module, with_more_info=with_more_info) + return get_logger(module=module, with_more_info=with_more_info, allow_site=allow_site, filter=filter, max_size=max_size, file_count=file_count) def log_error(message=None, title=_("Error")): '''Log error to Error Log''' diff --git a/frappe/app.py b/frappe/app.py index 57db867882..725bec183a 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -99,7 +99,7 @@ def application(request): frappe.monitor.stop(response) frappe.recorder.dump() - frappe.logger("frappe.web").info({ + frappe.logger("frappe.web", allow_site=frappe.local.site).info({ "site": get_site_name(request.host), "remote_addr": getattr(request, "remote_addr", "NOTFOUND"), "base_url": getattr(request, "base_url", "NOTFOUND"), @@ -256,9 +256,11 @@ def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=No 'SERVER_NAME': 'localhost:8000' } + log = logging.getLogger('werkzeug') + log.propagate = False + in_test_env = os.environ.get('CI') if in_test_env: - log = logging.getLogger('werkzeug') log.setLevel(logging.ERROR) run_simple('0.0.0.0', int(port), application, diff --git a/frappe/auth.py b/frappe/auth.py index 1353acf10f..64fea36748 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -333,12 +333,20 @@ class CookieManager: # sid expires in 3 days expires = datetime.datetime.now() + datetime.timedelta(days=3) if frappe.session.sid: - self.cookies["sid"] = {"value": frappe.session.sid, "expires": expires} + self.set_cookie("sid", frappe.session.sid, expires=expires, httponly=True) if frappe.session.session_country: - self.cookies["country"] = {"value": frappe.session.get("session_country")} + self.set_cookie("country", frappe.session.session_country) - def set_cookie(self, key, value, expires=None): - self.cookies[key] = {"value": value, "expires": expires} + def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"): + if not secure: + secure = frappe.local.request.scheme == "https" + self.cookies[key] = { + "value": value, + "expires": expires, + "secure": secure, + "httponly": httponly, + "samesite": samesite + } def delete_cookie(self, to_delete): if not isinstance(to_delete, (list, tuple)): @@ -349,7 +357,10 @@ class CookieManager: def flush_cookies(self, response): for key, opts in self.cookies.items(): response.set_cookie(key, quote((opts.get("value") or "").encode('utf-8')), - expires=opts.get("expires")) + expires=opts.get("expires"), + secure=opts.get("secure"), + httponly=opts.get("httponly"), + samesite=opts.get("samesite")) # expires yesterday! expires = datetime.datetime.now() + datetime.timedelta(days=-1) diff --git a/frappe/automation/desk_page/tools/tools.json b/frappe/automation/desk_page/tools/tools.json index 2164a4ce38..3fbaf62d02 100644 --- a/frappe/automation/desk_page/tools/tools.json +++ b/frappe/automation/desk_page/tools/tools.json @@ -3,7 +3,7 @@ { "hidden": 0, "label": "Tools", - "links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Video\",\n \"name\": \"Video\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]" + "links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]" }, { "hidden": 0, @@ -29,10 +29,11 @@ "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "Tools", - "modified": "2020-04-20 18:21:14.152537", + "modified": "2020-07-21 19:32:18.480700", "modified_by": "Administrator", "module": "Automation", "name": "Tools", diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index c447c55727..a946fcc81c 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -146,7 +146,7 @@ class AutoRepeat(Document): def make_new_document(self): reference_doc = frappe.get_doc(self.reference_doctype, self.reference_document) - new_doc = frappe.copy_doc(reference_doc, ignore_no_copy = False) + new_doc = frappe.copy_doc(reference_doc) self.update_doc(new_doc, reference_doc) new_doc.insert(ignore_permissions = True) @@ -372,7 +372,8 @@ def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, e doc.save() return doc -#method for reference_doctype filter +# method for reference_doctype filter +@frappe.whitelist() def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters): res = frappe.db.get_all('Property Setter', { 'property': 'allow_auto_repeat', diff --git a/frappe/boot.py b/frappe/boot.py index 8862ce3c61..b552d7d703 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -107,7 +107,7 @@ def load_desktop_data(bootinfo): from frappe.config import get_modules_from_all_apps_for_user from frappe.desk.desktop import get_desk_sidebar_items bootinfo.allowed_modules = get_modules_from_all_apps_for_user() - bootinfo.allowed_workspaces = get_desk_sidebar_items(True) + bootinfo.allowed_workspaces = get_desk_sidebar_items(flatten=True, cache=False) bootinfo.module_page_map = get_controller("Desk Page").get_module_page_map() bootinfo.dashboards = frappe.get_all("Dashboard") diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index 92d12289c6..97b6c235b5 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -21,7 +21,7 @@ global_cache_keys = ("app_hooks", "installed_apps", user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang", "defaults", "user_permissions", "home_page", "linked_with", "desktop_icons", 'portal_menu_items', 'user_perm_can_read', - "has_role:Page", "has_role:Report") + "has_role:Page", "has_role:Report", "desk_sidebar_items") doctype_cache_keys = ("meta", "form_meta", "table_columns", "last_modified", "linked_doctypes", 'notifications', 'workflow' ,'energy_point_rule_map', 'data_import_column_header_map') diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 0f51f21104..26eb455338 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -201,16 +201,31 @@ def _reinstall(site, admin_password=None, mariadb_root_username=None, mariadb_ro def install_app(context, apps): "Install a new app to site, supports multiple apps" from frappe.installer import install_app as _install_app + exit_code = 0 + + if not context.sites: + raise SiteNotSpecifiedError + for site in context.sites: frappe.init(site=site) frappe.connect() - try: - for app in apps: + + for app in apps: + try: _install_app(app, verbose=context.verbose) - finally: - frappe.destroy() - if not context.sites: - raise SiteNotSpecifiedError + except frappe.IncompatibleApp as err: + err_msg = ":\n{}".format(err) if str(err) else "" + print("App {} is Incompatible with Site {}{}".format(app, site, err_msg)) + exit_code = 1 + except Exception as err: + err_msg = ":\n{}".format(err if str(err) else frappe.get_traceback()) + print("An error occurred while installing {}{}".format(app, err_msg)) + exit_code = 1 + + frappe.destroy() + + sys.exit(exit_code) + @click.command('list-apps') @pass_context @@ -422,15 +437,16 @@ def remove_from_installed_apps(context, app): @click.option('--yes', '-y', help='To bypass confirmation prompt for uninstalling the app', is_flag=True, default=False, multiple=True) @click.option('--dry-run', help='List all doctypes that will be deleted', is_flag=True, default=False) @click.option('--no-backup', help='Do not backup the site', is_flag=True, default=False) +@click.option('--force', help='Force remove app from site', is_flag=True, default=False) @pass_context -def uninstall(context, app, dry_run=False, yes=False, no_backup=False): +def uninstall(context, app, dry_run, yes, no_backup, force): "Remove app and linked modules from site" from frappe.installer import remove_app for site in context.sites: try: frappe.init(site=site) frappe.connect() - remove_app(app, dry_run, yes, no_backup) + remove_app(app_name=app, dry_run=dry_run, yes=yes, no_backup=no_backup, force=force) finally: frappe.destroy() if not context.sites: @@ -615,6 +631,29 @@ def stop_recording(context): if not context.sites: raise SiteNotSpecifiedError +@click.command('ngrok') +@pass_context +def start_ngrok(context): + from pyngrok import ngrok + + site = get_site(context) + frappe.init(site=site) + + port = frappe.conf.http_port or frappe.conf.webserver_port + public_url = ngrok.connect(port=port, options={ + 'host_header': site + }) + print(f'Public URL: {public_url}') + print('Inspect logs at http://localhost:4040') + + ngrok_process = ngrok.get_ngrok_process() + try: + # Block until CTRL-C or some other terminating event + ngrok_process.proc.wait() + except KeyboardInterrupt: + print("Shutting down server...") + frappe.destroy() + ngrok.kill() commands = [ add_system_manager, @@ -640,5 +679,6 @@ commands = [ browse, start_recording, stop_recording, - add_to_hosts + add_to_hosts, + start_ngrok ] diff --git a/frappe/config/integrations.py b/frappe/config/integrations.py index f41adc9ea4..a7ac20065f 100644 --- a/frappe/config/integrations.py +++ b/frappe/config/integrations.py @@ -27,6 +27,11 @@ def get_data(): "name": "Stripe Settings", "description": _("Stripe payment gateway settings"), }, + { + "type": "doctype", + "name": "Paytm Settings", + "description": _("Paytm payment gateway settings"), + }, ] }, { diff --git a/frappe/config/settings.py b/frappe/config/settings.py index 848ef2e1aa..e43abd9fcb 100644 --- a/frappe/config/settings.py +++ b/frappe/config/settings.py @@ -16,6 +16,13 @@ def get_data(): "description": _("Language, Date and Time settings"), "hide_count": True }, + { + "type": "doctype", + "name": "Global Defaults", + "label": _("Global Defaults"), + "description": _("Company, Fiscal Year and Currency defaults"), + "hide_count": True + }, { "type": "doctype", "name": "Error Log", diff --git a/frappe/contacts/address_and_contact.py b/frappe/contacts/address_and_contact.py index 5a004f153b..51f13fb1a1 100644 --- a/frappe/contacts/address_and_contact.py +++ b/frappe/contacts/address_and_contact.py @@ -146,6 +146,7 @@ def delete_contact_and_address(doctype, docname): if len(doc.links)==1: doc.delete() +@frappe.whitelist() def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, filters): if not txt: txt = "" diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py index b85d578353..57dea8284c 100644 --- a/frappe/contacts/doctype/address/address.py +++ b/frappe/contacts/doctype/address/address.py @@ -230,6 +230,7 @@ def get_company_address(company): return ret +@frappe.whitelist() def address_query(doctype, txt, searchfield, start, page_len, filters): from frappe.desk.reportview import get_match_cond @@ -237,16 +238,17 @@ def address_query(doctype, txt, searchfield, start, page_len, filters): link_name = filters.pop('link_name') condition = "" - for fieldname, value in iteritems(filters): - condition += " and {field}={value}".format( - field=fieldname, - value=value - ) - meta = frappe.get_meta("Address") + for fieldname, value in iteritems(filters): + if meta.get_field(fieldname) or fieldname in frappe.db.DEFAULT_COLUMNS: + condition += " and {field}={value}".format( + field=fieldname, + value=frappe.db.escape(value)) + searchfields = meta.get_search_fields() - if searchfield: + if searchfield and (meta.get_field(searchfield)\ + or searchfield in frappe.db.DEFAULT_COLUMNS): searchfields.append(searchfield) search_condition = '' @@ -289,4 +291,4 @@ def get_condensed_address(doc): return ", ".join([doc.get(d) for d in fields if doc.get(d)]) def update_preferred_address(address, field): - frappe.db.set_value('Address', address, field, 0) \ No newline at end of file + frappe.db.set_value('Address', address, field, 0) diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index 4cf209541c..8240940d2f 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -182,19 +182,17 @@ def update_contact(doc, method): contact.flags.ignore_mandatory = True contact.save(ignore_permissions=True) +@frappe.whitelist() def contact_query(doctype, txt, searchfield, start, page_len, filters): from frappe.desk.reportview import get_match_cond + if not frappe.get_meta("Contact").get_field(searchfield)\ + or searchfield not in frappe.db.DEFAULT_COLUMNS: + return [] + link_doctype = filters.pop('link_doctype') link_name = filters.pop('link_name') - condition = "" - for fieldname, value in iteritems(filters): - condition += " and {field}={value}".format( - field=fieldname, - value=value - ) - return frappe.db.sql("""select `tabContact`.name, `tabContact`.first_name, `tabContact`.last_name from @@ -209,9 +207,7 @@ def contact_query(doctype, txt, searchfield, start, page_len, filters): order by if(locate(%(_txt)s, `tabContact`.name), locate(%(_txt)s, `tabContact`.name), 99999), `tabContact`.idx desc, `tabContact`.name - limit %(start)s, %(page_len)s """.format( - mcond=get_match_cond(doctype), - key=searchfield), { + limit %(start)s, %(page_len)s """.format(mcond=get_match_cond(doctype), key=searchfield), { 'txt': '%' + txt + '%', '_txt': txt.replace("%", ""), 'start': start, diff --git a/frappe/core/desk_page/settings/settings.json b/frappe/core/desk_page/settings/settings.json index 6569b2fb20..642a4fdadd 100644 --- a/frappe/core/desk_page/settings/settings.json +++ b/frappe/core/desk_page/settings/settings.json @@ -18,7 +18,7 @@ { "hidden": 0, "label": "Core", - "links": "[\n {\n \"description\": \"Language, Date and Time settings\",\n \"hide_count\": true,\n \"label\": \"System Settings\",\n \"name\": \"System Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error on automated events (scheduler).\",\n \"label\": \"Error Log\",\n \"name\": \"Error Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error during requests.\",\n \"label\": \"Error Snapshot\",\n \"name\": \"Error Snapshot\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Enable / Disable Domains\",\n \"hide_count\": true,\n \"label\": \"Domain Settings\",\n \"name\": \"Domain Settings\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"description\": \"Language, Date and Time settings\",\n \"hide_count\": true,\n \"label\": \"System Settings\",\n \"name\": \"System Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Company, Fiscal Year and Currency defaults\",\n \"hide_count\": true,\n \"label\": \"Global Defaults\",\n \"name\": \"Global Defaults\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error on automated events (scheduler).\",\n \"label\": \"Error Log\",\n \"name\": \"Error Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error during requests.\",\n \"label\": \"Error Snapshot\",\n \"name\": \"Error Snapshot\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Enable / Disable Domains\",\n \"hide_count\": true,\n \"label\": \"Domain Settings\",\n \"name\": \"Domain Settings\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -39,10 +39,11 @@ "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "Settings", - "modified": "2020-04-01 11:24:40.636747", + "modified": "2020-07-14 10:09:09.520557", "modified_by": "Administrator", "module": "Core", "name": "Settings", diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index daf64d4b8b..4c531fbac6 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -221,7 +221,7 @@ def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None) :param print_html: Send given value as HTML attachment. :param print_format: Attach print format of parent document.""" - view_link = frappe.utils.cint(frappe.db.get_value("Print Settings", "Print Settings", "attach_view_link")) + view_link = frappe.utils.cint(frappe.db.get_value("System Settings", "System Settings", "attach_view_link")) if print_format and view_link: doc.content += get_attach_link(doc, print_format) @@ -236,7 +236,7 @@ def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None) if doc.sender: # combine for sending to get the format 'Jane ' - doc.sender = formataddr([doc.sender_full_name, doc.sender]) + doc.sender = get_formatted_email(doc.sender_full_name, mail=doc.sender) doc.attachments = [] diff --git a/frappe/core/doctype/data_import/data_import.js b/frappe/core/doctype/data_import/data_import.js index 81a7bc9705..6a922618cb 100644 --- a/frappe/core/doctype/data_import/data_import.js +++ b/frappe/core/doctype/data_import/data_import.js @@ -102,6 +102,10 @@ frappe.ui.form.on('Data Import', { }, update_primary_action(frm) { + if (frm.is_dirty()) { + frm.enable_save(); + return; + } frm.disable_save(); if (frm.doc.status !== 'Success') { if (!frm.is_new() && (frm.has_import_file())) { @@ -199,20 +203,12 @@ frappe.ui.form.on('Data Import', { }, download_template(frm) { - if ( - frm.data_exporter && - frm.data_exporter.doctype === frm.doc.reference_doctype - ) { - frm.data_exporter.exporting_for = frm.doc.import_type; - frm.data_exporter.dialog.show(); - } else { - frappe.require('/assets/js/data_import_tools.min.js', () => { - frm.data_exporter = new frappe.data_import.DataExporter( - frm.doc.reference_doctype, - frm.doc.import_type - ); - }); - } + frappe.require('/assets/js/data_import_tools.min.js', () => { + frm.data_exporter = new frappe.data_import.DataExporter( + frm.doc.reference_doctype, + frm.doc.import_type + ); + }); }, reference_doctype(frm) { @@ -301,8 +297,8 @@ frappe.ui.form.on('Data Import', { events: { remap_column(changed_map) { let template_options = JSON.parse(frm.doc.template_options || '{}'); - template_options.remap_column = template_options.remap_column || {}; - Object.assign(template_options.remap_column, changed_map); + template_options.column_to_field_map = template_options.column_to_field_map || {}; + Object.assign(template_options.column_to_field_map, changed_map); frm.set_value('template_options', JSON.stringify(template_options)); frm.save().then(() => frm.trigger('import_file')); } @@ -435,10 +431,10 @@ frappe.ui.form.on('Data Import', { .join(''); let id = frappe.dom.get_unique_id(); html = `${messages} - -
+
${log.exception}
diff --git a/frappe/core/doctype/data_import/data_import.json b/frappe/core/doctype/data_import/data_import.json index 177252ea22..8b1b6c4e07 100644 --- a/frappe/core/doctype/data_import/data_import.json +++ b/frappe/core/doctype/data_import/data_import.json @@ -119,7 +119,7 @@ { "fieldname": "import_warnings_section", "fieldtype": "Section Break", - "label": "Warnings" + "label": "Import File Errors and Warnings" }, { "fieldname": "import_warnings", @@ -127,7 +127,7 @@ "label": "Import Warnings" }, { - "depends_on": "reference_doctype", + "depends_on": "eval:!doc.__islocal", "fieldname": "download_template", "fieldtype": "Button", "label": "Download Template" @@ -159,7 +159,7 @@ "label": "Import from Google Sheets" }, { - "depends_on": "eval:doc.google_sheets_url", + "depends_on": "eval:doc.google_sheets_url && !doc.__unsaved", "fieldname": "refresh_google_sheet", "fieldtype": "Button", "label": "Refresh Google Sheet" @@ -167,7 +167,7 @@ ], "hide_toolbar": 1, "links": [], - "modified": "2020-06-18 16:05:54.211034", + "modified": "2020-06-24 14:33:03.173876", "modified_by": "Administrator", "module": "Core", "name": "Data Import", diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 4761652c70..14626eb5e3 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -16,6 +16,7 @@ from frappe.utils.xlsxutils import ( read_xls_file_from_attached_file, ) from frappe.model import no_value_fields, table_fields as table_fieldtypes +from frappe.core.doctype.version.version import get_diff INVALID_VALUES = ("", None) MAX_ROWS_IN_PREVIEW = 10 @@ -58,6 +59,7 @@ class Importer: frappe.flags.in_import = True frappe.flags.mute_emails = self.data_import.mute_emails + self.data_import.db_set("status", "Pending") self.data_import.db_set("template_warnings", "") def import_data(self): @@ -216,14 +218,22 @@ class Importer: def update_record(self, doc): id_field = get_id_field(self.doctype) existing_doc = frappe.get_doc(self.doctype, doc.get(id_field.fieldname)) - existing_doc.flags.updater_reference = { - "doctype": self.data_import.doctype, - "docname": self.data_import.name, - "label": _("via Data Import"), - } - existing_doc.update(doc) - existing_doc.save() - return existing_doc + + updated_doc = frappe.get_doc(self.doctype, doc.get(id_field.fieldname)) + updated_doc.update(doc) + + if get_diff(existing_doc, updated_doc): + # update doc if there are changes + updated_doc.flags.updater_reference = { + "doctype": self.data_import.doctype, + "docname": self.data_import.name, + "label": _("via Data Import"), + } + updated_doc.save() + return updated_doc + else: + # throw if no changes + frappe.throw('No changes to update') def get_eta(self, current, total, processing_time): self.last_eta = getattr(self, "last_eta", 0) @@ -306,8 +316,9 @@ class ImportFile: ) self.column_to_field_map = self.template_options.column_to_field_map self.import_type = import_type + self.warnings = [] - self.file_doc = self.file_path = None + self.file_doc = self.file_path = self.google_sheets_url = None if isinstance(file, frappe.string_types): if frappe.db.exists("File", {"file_url": file}): self.file_doc = frappe.get_doc("File", {"file_url": file}) @@ -430,9 +441,8 @@ class ImportFile: # if there are child doctypes, find the subsequent rows if len(doctypes) > 1: - # subsequent rows either dont have any parent value set - # or have the same value as the parent row - # we include a row if either of conditions match + # subsequent rows that have blank values in parent columns + # are considered as child rows parent_column_indexes = self.header.get_column_indexes(self.doctype) parent_row_values = first_row.get_values(parent_column_indexes) @@ -443,11 +453,8 @@ class ImportFile: if all([v in INVALID_VALUES for v in row_values]): rows.append(row) continue - # if the row has same values as parent row, it's a child row doc - if row_values == parent_row_values: - rows.append(row) - continue - # if any of those conditions dont match, it's the next doc + # if we encounter a row which has values in parent columns, + # then it is the next doc break parent_doc = None @@ -462,38 +469,46 @@ class ImportFile: parent_doc[table_df.fieldname].append(child_doc) doc = parent_doc - # check if there is atleast one row for mandatory table fields - meta = frappe.get_meta(self.doctype) - mandatory_table_fields = [ - df - for df in meta.fields - if df.fieldtype in table_fieldtypes - and df.reqd - and len(doc.get(df.fieldname, [])) == 0 - ] - if len(mandatory_table_fields) == 1: - self.warnings.append( - { - "row": first_row.row_number, - "message": _("There should be atleast one row for {0} table").format( - mandatory_table_fields[0].label - ), - } - ) - elif mandatory_table_fields: - fields_string = ", ".join([df.label for df in mandatory_table_fields]) - message = _("There should be atleast one row for the following tables: {0}").format( - fields_string - ) - self.warnings.append({"row": first_row.row_number, "message": message}) + + if self.import_type == INSERT: + # check if there is atleast one row for mandatory table fields + meta = frappe.get_meta(self.doctype) + mandatory_table_fields = [ + df + for df in meta.fields + if df.fieldtype in table_fieldtypes + and df.reqd + and len(doc.get(df.fieldname, [])) == 0 + ] + if len(mandatory_table_fields) == 1: + self.warnings.append( + { + "row": first_row.row_number, + "message": _("There should be atleast one row for {0} table").format( + frappe.bold(mandatory_table_fields[0].label) + ), + } + ) + elif mandatory_table_fields: + fields_string = ", ".join([df.label for df in mandatory_table_fields]) + message = _("There should be atleast one row for the following tables: {0}").format( + fields_string + ) + self.warnings.append({"row": first_row.row_number, "message": message}) return doc, rows, data[len(rows) :] def get_warnings(self): warnings = [] + + # ImportFile warnings + warnings += self.warnings + + # Column warnings for col in self.header.columns: warnings += col.warnings + # Row warnings for row in self.data: warnings += row.warnings @@ -600,14 +615,14 @@ class Row: def validate_value(self, value, col): df = col.df if df.fieldtype == "Select": - select_options = df.get_select_options() + select_options = [d for d in (df.options or '').split('\n') if d] if select_options and value not in select_options: options_string = ", ".join([frappe.bold(d) for d in select_options]) msg = _("Value must be one of {0}").format(options_string) self.warnings.append( { "row": self.row_number, - "field": df.as_dict(convert_dates_to_str=True), + "field": df_as_json(df), "message": msg, } ) @@ -622,7 +637,7 @@ class Row: self.warnings.append( { "row": self.row_number, - "field": df.as_dict(convert_dates_to_str=True), + "field": df_as_json(df), "message": msg, } ) @@ -635,7 +650,7 @@ class Row: { "row": self.row_number, "col": col.column_number, - "field": df.as_dict(convert_dates_to_str=True), + "field": df_as_json(df), "message": _("Value {0} must in {1} format").format( frappe.bold(value), frappe.bold(get_user_format(col.date_format)) ), @@ -646,7 +661,7 @@ class Row: return value def link_exists(self, value, df): - key = df.options + "::" + value + key = df.options + "::" + cstr(value) if Row.link_values_exist_map.get(key) is None: Row.link_values_exist_map[key] = frappe.db.exists(df.options, value) return Row.link_values_exist_map.get(key) @@ -674,6 +689,9 @@ class Row: return value def get_date(self, value, column): + if isinstance(value, datetime): + return value + date_format = column.date_format if date_format: try: @@ -755,19 +773,21 @@ class Row: class Header(Row): - def __init__(self, index, row, doctype, raw_data, column_to_field_map): + def __init__(self, index, row, doctype, raw_data, column_to_field_map=None): self.index = index self.row_number = index + 1 self.data = row self.doctype = doctype + column_to_field_map = column_to_field_map or frappe._dict() self.seen = [] self.columns = [] for j, header in enumerate(row): column_values = [get_item_at_index(r, j) for r in raw_data] + map_to_field = column_to_field_map.get(str(j)) column = Column( - j, header, self.doctype, column_values, column_to_field_map.get(header), self.seen + j, header, self.doctype, column_values, map_to_field, self.seen ) self.seen.append(header) self.columns.append(column) @@ -824,7 +844,7 @@ class Column: self.meta = frappe.get_meta(doctype) self.parse() - self.parse_date_format() + self.validate_values() def parse(self): header_title = self.header_title @@ -897,10 +917,6 @@ class Column: self.df = df self.skip_import = skip_import - def parse_date_format(self): - if self.df and self.df.fieldtype in ("Date", "Time", "Datetime"): - self.date_format = self.guess_date_format_for_column() - def guess_date_format_for_column(self): """ Guesses date format for a column by parsing all the values in the column, getting the date format and then returning the one which has the maximum frequency @@ -935,6 +951,36 @@ class Column: return max_occurred_date_format + def validate_values(self): + if not self.df: + return + + if self.skip_import: + return + + if self.df.fieldtype == 'Link': + # find all values that dont exist + values = list(set([cstr(v) for v in self.column_values[1:] if v])) + exists = [d.name for d in frappe.db.get_all(self.df.options, filters={'name': ('in', values)})] + not_exists = list(set(values) - set(exists)) + if not_exists: + missing_values = ', '.join(not_exists) + self.warnings.append({ + 'col': self.column_number, + 'message': "The following values do not exist for {}: {}".format(self.df.options, missing_values), + 'type': 'warning' + }) + elif self.df.fieldtype in ("Date", "Time", "Datetime"): + # guess date format + self.date_format = self.guess_date_format_for_column() + if not self.date_format: + self.date_format = '%Y-%m-%d' + self.warnings.append({ + 'col': self.column_number, + 'message': _("Date format could not determined from the values in this column. Defaulting to yyyy-mm-dd."), + 'type': 'info' + }) + def as_dict(self): d = frappe._dict() d.index = self.index @@ -944,6 +990,9 @@ class Column: d.map_to_field = self.map_to_field d.date_format = self.date_format d.df = self.df + if hasattr(self.df, 'is_child_table_field'): + d.is_child_table_field = self.df.is_child_table_field + d.child_table_df = self.df.child_table_df d.skip_import = self.skip_import d.warnings = self.warnings return d @@ -1021,6 +1070,7 @@ def build_fields_dict_for_column_matching(parent_doctype): # other fields fields = get_standard_fields(doctype) + frappe.get_meta(doctype).fields for df in fields: + label = (df.label or '').strip() fieldtype = df.fieldtype or "Data" parent = df.parent or parent_doctype if fieldtype not in no_value_fields: @@ -1029,12 +1079,12 @@ def build_fields_dict_for_column_matching(parent_doctype): # Label # label # Label (label) - if not out.get(df.label): + if not out.get(label): # if Label is already set, don't set it again # in case of duplicate column headers - out[df.label] = df + out[label] = df out[df.fieldname] = df - label_with_fieldname = "{0} ({1})".format(df.label, df.fieldname) + label_with_fieldname = "{0} ({1})".format(label, df.fieldname) out[label_with_fieldname] = df else: # in case there are multiple table fields with the same doctype @@ -1045,7 +1095,7 @@ def build_fields_dict_for_column_matching(parent_doctype): "fields", {"fieldtype": ["in", table_fieldtypes], "options": parent} ) for table_field in table_fields: - by_label = "{0} ({1})".format(df.label, table_field.label) + by_label = "{0} ({1})".format(label, table_field.label) by_fieldname = "{0}.{1}".format(table_field.fieldname, df.fieldname) # create a new df object to avoid mutation problems @@ -1113,3 +1163,13 @@ def get_user_format(date_format): .replace("%m", "mm") .replace("%d", "dd") ) + +def df_as_json(df): + return { + 'fieldname': df.fieldname, + 'fieldtype': df.fieldtype, + 'label': df.label, + 'options': df.options, + 'parent': df.parent, + 'default': df.default + } diff --git a/frappe/core/doctype/file/file.json b/frappe/core/doctype/file/file.json index d9ab504db7..3008e27aa0 100644 --- a/frappe/core/doctype/file/file.json +++ b/frappe/core/doctype/file/file.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "creation": "2012-12-12 11:19:22", "doctype": "DocType", @@ -63,7 +64,8 @@ "fieldname": "is_home_folder", "fieldtype": "Check", "hidden": 1, - "label": "Is Home Folder" + "label": "Is Home Folder", + "search_index": 1 }, { "default": "0", @@ -172,7 +174,8 @@ ], "icon": "fa fa-file", "idx": 1, - "modified": "2019-08-30 19:46:20.796453", + "links": [], + "modified": "2020-06-28 12:21:30.772386", "modified_by": "Administrator", "module": "Core", "name": "File", diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 831d2ab22d..1748c60020 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -100,26 +100,26 @@ class File(Document): self.validate_file() self.generate_content_hash() - self.validate_url() - if frappe.db.exists('File', {'name': self.name, 'is_folder': 0}): old_file_url = self.file_url if not self.is_folder and (self.is_private != self.db_get('is_private')): private_files = frappe.get_site_path('private', 'files') public_files = frappe.get_site_path('public', 'files') + file_name = self.file_url.split('/')[-1] if not self.is_private: - shutil.move(os.path.join(private_files, self.file_name), - os.path.join(public_files, self.file_name)) + shutil.move(os.path.join(private_files, file_name), + os.path.join(public_files, file_name)) - self.file_url = "/files/{0}".format(self.file_name) + self.file_url = "/files/{0}".format(file_name) else: - shutil.move(os.path.join(public_files, self.file_name), - os.path.join(private_files, self.file_name)) + shutil.move(os.path.join(public_files, file_name), + os.path.join(private_files, file_name)) - self.file_url = "/private/files/{0}".format(self.file_name) + self.file_url = "/private/files/{0}".format(file_name) + update_existing_file_docs(self) # update documents image url with new file url if self.attached_to_doctype and self.attached_to_name: @@ -135,6 +135,8 @@ class File(Document): frappe.db.set_value(self.attached_to_doctype, self.attached_to_name, self.attached_to_field, self.file_url) + self.validate_url() + if self.file_url and (self.is_private != self.file_url.startswith('/private')): frappe.throw(_('Invalid file URL. Please contact System Administrator.')) @@ -182,13 +184,7 @@ class File(Document): if duplicate_file: duplicate_file_doc = frappe.get_cached_doc('File', duplicate_file.name) if duplicate_file_doc.exists_on_disk(): - # if it is attached to a document then throw FileAlreadyAttachedException - if self.attached_to_doctype and self.attached_to_name: - self.duplicate_entry = duplicate_file.name - frappe.throw(_("Same file has already been attached to the record"), - frappe.FileAlreadyAttachedException) - # else just use the url, to avoid uploading a duplicate - else: + # just use the url, to avoid uploading a duplicate self.file_url = duplicate_file.file_url def set_file_name(self): @@ -909,3 +905,20 @@ def get_files_in_folder(folder): { 'folder': folder }, ['name', 'file_name', 'file_url', 'is_folder', 'modified'] ) + +def update_existing_file_docs(doc): + # Update is private and file url of all file docs that point to the same file + frappe.db.sql(""" + UPDATE `tabFile` + SET + file_url = %(file_url)s, + is_private = %(is_private)s + WHERE + content_hash = %(content_hash)s + and name != %(file_name)s + """, dict( + file_url=doc.file_url, + is_private=doc.is_private, + content_hash=doc.content_hash, + file_name=doc.name + )) diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py index cc9628ed5b..ec4f97bf67 100644 --- a/frappe/core/doctype/file/test_file.py +++ b/frappe/core/doctype/file/test_file.py @@ -294,4 +294,37 @@ class TestFile(unittest.TestCase): folder = frappe.get_doc("File", "Home/Test Folder 1/Test Folder 3") self.assertRaises(frappe.ValidationError, folder.delete) + def test_same_file_url_update(self): + attached_to_doctype1, attached_to_docname1 = make_test_doc() + attached_to_doctype2, attached_to_docname2 = make_test_doc() + + file1 = frappe.get_doc({ + "doctype": "File", + "file_name": 'file1.txt', + "attached_to_doctype": attached_to_doctype1, + "attached_to_name": attached_to_docname1, + "is_private": 1, + "content": test_content1}).insert() + + file2 = frappe.get_doc({ + "doctype": "File", + "file_name": 'file2.txt', + "attached_to_doctype": attached_to_doctype2, + "attached_to_name": attached_to_docname2, + "is_private": 1, + "content": test_content1}).insert() + + self.assertEqual(file1.is_private, file2.is_private, 1) + self.assertEqual(file1.file_url, file2.file_url) + self.assertTrue(os.path.exists(file1.get_full_path())) + + file1.is_private = 0 + file1.save() + + file2 = frappe.get_doc('File', file2.name) + + self.assertEqual(file1.is_private, file2.is_private, 0) + self.assertEqual(file1.file_url, file2.file_url) + self.assertTrue(os.path.exists(file2.get_full_path())) + diff --git a/frappe/core/doctype/module_def/module_def.py b/frappe/core/doctype/module_def/module_def.py index 0c0e7c4f45..755cb86dbe 100644 --- a/frappe/core/doctype/module_def/module_def.py +++ b/frappe/core/doctype/module_def/module_def.py @@ -42,6 +42,10 @@ class ModuleDef(Document): def on_trash(self): """Delete module name from modules.txt""" + + if frappe.flags.in_uninstall: + return + modules = None if frappe.local.module_app.get(frappe.scrub(self.name)): with open(frappe.get_app_path(self.app_name, "modules.txt"), "r") as f: diff --git a/frappe/core/doctype/report/report.js b/frappe/core/doctype/report/report.js index 818c5951e6..c410e9aa1a 100644 --- a/frappe/core/doctype/report/report.js +++ b/frappe/core/doctype/report/report.js @@ -1,6 +1,6 @@ frappe.ui.form.on('Report', { refresh: function(frm) { - if(!frappe.boot.developer_mode && frappe.session.user !== 'Administrator') { + if (frm.doc.is_standard === "Yes" && !frappe.boot.developer_mode) { // make the document read-only frm.set_read_only(); } diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index 3e6b7a3a98..5c12858e8a 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -52,9 +52,10 @@ class TestServerScript(unittest.TestCase): frappe.db.commit() - # @classmethod - # def tearDownClass(cls): - # frappe.db.sql('truncate `tabServer Script`') + @classmethod + def tearDownClass(cls): + frappe.db.commit() + frappe.db.sql('truncate `tabServer Script`') def setUp(self): frappe.cache().delete_value('server_script_map') diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 1d0cda95a4..b2cb67dbc9 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -59,6 +59,7 @@ "column_break_18", "disable_standard_email_footer", "hide_footer_in_auto_email_reports", + "attach_view_link", "chat", "enable_chat", "use_socketio_to_upload_file" @@ -422,12 +423,18 @@ "fieldname": "enable_onboarding", "fieldtype": "Check", "label": "Enable Onboarding" + }, + { + "default": "1", + "fieldname": "attach_view_link", + "fieldtype": "Check", + "label": "Send document Web View link in email" } ], "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2020-05-01 19:21:15.496065", + "modified": "2020-07-02 16:13:00.166382", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 7b9266ff64..64bff32189 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals, print_function import frappe from frappe.model.document import Document -from frappe.utils import cint, flt, has_gravatar, format_datetime, now_datetime, get_formatted_email, today +from frappe.utils import cint, flt, has_gravatar, escape_html, format_datetime, now_datetime, get_formatted_email, today from frappe import throw, msgprint, _ from frappe.utils.password import update_password as _update_password from frappe.desk.notifications import clear_notifications @@ -770,7 +770,7 @@ def sign_up(email, full_name, redirect_to): user = frappe.get_doc({ "doctype":"User", "email": email, - "first_name": full_name, + "first_name": escape_html(full_name), "enabled": 1, "new_password": random_string(10), "user_type": "Website User" @@ -811,6 +811,7 @@ def reset_password(user): frappe.clear_messages() return 'not found' +@frappe.whitelist() def user_query(doctype, txt, searchfield, start, page_len, filters): from frappe.desk.reportview import get_match_cond diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py index 48d4fcb5d4..325c41622d 100644 --- a/frappe/core/doctype/user_permission/user_permission.py +++ b/frappe/core/doctype/user_permission/user_permission.py @@ -119,6 +119,7 @@ def user_permission_exists(user, allow, for_value, applicable_for=None): return has_same_user_permission +@frappe.whitelist() def get_applicable_for_doctype_list(doctype, txt, searchfield, start, page_len, filters): linked_doctypes_map = get_linked_doctypes(doctype, True) diff --git a/frappe/core/doctype/video/video.js b/frappe/core/doctype/video/video.js deleted file mode 100644 index 36ea240a36..0000000000 --- a/frappe/core/doctype/video/video.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2020, Frappe Technologies and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Video', { - // refresh: function(frm) { - - // } -}); diff --git a/frappe/core/doctype/video/video.json b/frappe/core/doctype/video/video.json deleted file mode 100644 index 26a407c05c..0000000000 --- a/frappe/core/doctype/video/video.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "actions": [], - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:title", - "creation": "2018-10-17 05:47:13.087395", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "title", - "provider", - "url", - "column_break_4", - "publish_date", - "duration", - "section_break_7", - "description" - ], - "fields": [ - { - "fieldname": "title", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Title", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "provider", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Provider", - "options": "YouTube\nVimeo", - "reqd": 1 - }, - { - "fieldname": "url", - "fieldtype": "Data", - "in_list_view": 1, - "label": "URL", - "reqd": 1 - }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, - { - "fieldname": "publish_date", - "fieldtype": "Date", - "label": "Publish Date" - }, - { - "fieldname": "duration", - "fieldtype": "Data", - "label": "Duration" - }, - { - "fieldname": "section_break_7", - "fieldtype": "Section Break" - }, - { - "fieldname": "description", - "fieldtype": "Text Editor", - "in_list_view": 1, - "label": "Description", - "reqd": 1 - } - ], - "links": [], - "modified": "2020-04-22 12:09:49.057403", - "modified_by": "Administrator", - "module": "Core", - "name": "Video", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "All", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/frappe/core/doctype/video/video.py b/frappe/core/doctype/video/video.py deleted file mode 100644 index fdbd3a1abe..0000000000 --- a/frappe/core/doctype/video/video.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- 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 Video(Document): - pass diff --git a/frappe/core/page/dashboard/dashboard.css b/frappe/core/page/dashboard/dashboard.css deleted file mode 100644 index b319cc1ed2..0000000000 --- a/frappe/core/page/dashboard/dashboard.css +++ /dev/null @@ -1,5 +0,0 @@ -.restricted-button { - cursor: default; - position: relative; - right: -5px; -} \ No newline at end of file diff --git a/frappe/core/page/dashboard/dashboard.js b/frappe/core/page/dashboard/dashboard.js index f17bc1e0b5..7e45163a7e 100644 --- a/frappe/core/page/dashboard/dashboard.js +++ b/frappe/core/page/dashboard/dashboard.js @@ -26,13 +26,6 @@ class Dashboard {
`).appendTo(this.wrapper.find(".page-content").empty()); this.container = this.wrapper.find(".dashboard-graph"); this.page = wrapper.page; - - this.page.set_title_sub( - $(``) - ); } show() { @@ -172,19 +165,26 @@ class Dashboard { set_dropdown() { this.page.clear_menu(); - this.page.add_menu_item('Edit...', () => { + this.page.add_menu_item(__('Edit'), () => { frappe.set_route('Form', 'Dashboard', frappe.dashboard.dashboard_name); - }, 1); + }); - this.page.add_menu_item('New...', () => { + this.page.add_menu_item(__('New'), () => { frappe.new_doc('Dashboard'); - }, 1); + }); - frappe.db.get_list("Dashboard").then(dashboards => { + this.page.add_menu_item(__('Refresh All'), () => { + this.chart_group && + this.chart_group.widgets_list.forEach(chart => chart.refresh()); + this.number_card_group && + this.number_card_group.widgets_list.forEach(card => card.render_card()); + }); + + frappe.db.get_list('Dashboard').then(dashboards => { dashboards.map(dashboard => { let name = dashboard.name; if(name != this.dashboard_name){ - this.page.add_menu_item(name, () => frappe.set_route("dashboard", name)); + this.page.add_menu_item(name, () => frappe.set_route("dashboard", name), 1); } }); }); diff --git a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py index 95a04360be..5ca21b811e 100644 --- a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py +++ b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py @@ -5,23 +5,23 @@ from __future__ import unicode_literals import frappe from frappe import _, throw import frappe.utils.user -from frappe.permissions import check_admin_or_system_manager +from frappe.permissions import check_admin_or_system_manager, rights from frappe.model import data_fieldtypes def execute(filters=None): user, doctype, show_permissions = filters.get("user"), filters.get("doctype"), filters.get("show_permissions") + if not validate(user, doctype): return [], [] columns, fields = get_columns_and_fields(doctype) data = frappe.get_list(doctype, fields=fields, as_list=True, user=user) if show_permissions: - columns = columns + ["Read", "Write", "Create", "Delete", "Submit", "Cancel", "Amend", "Print", "Email", - "Report", "Import", "Export", "Share"] + columns = columns + [frappe.unscrub(right) + ':Check:80' for right in rights] data = list(data) - for i,item in enumerate(data): - temp = frappe.permissions.get_doc_permissions(frappe.get_doc(doctype, item[0]), False,user) - data[i] = item+(temp.get("read"),temp.get("write"),temp.get("create"),temp.get("delete"),temp.get("submit"),temp.get("cancel"),temp.get("amend"),temp.get("print"),temp.get("email"),temp.get("report"),temp.get("import"),temp.get("export"),temp.get("share"),) + for i, doc in enumerate(data): + permission = frappe.permissions.get_doc_permissions(frappe.get_doc(doctype, doc[0]), user) + data[i] = doc + tuple(permission.get(right) for right in rights) return columns, data @@ -41,6 +41,7 @@ def get_columns_and_fields(doctype): return columns, fields +@frappe.whitelist() def query_doctypes(doctype, txt, searchfield, start, page_len, filters): user = filters.get("user") user_perms = frappe.utils.user.UserPermissions(user) diff --git a/frappe/database/db_manager.py b/frappe/database/db_manager.py index 80236b2dc2..3345fce735 100644 --- a/frappe/database/db_manager.py +++ b/frappe/database/db_manager.py @@ -49,7 +49,7 @@ class DbManager: host = self.get_current_host() if frappe.conf.get('rds_db', 0) == 1: - self.db.sql("GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES, CREATE VIEW, EVENT, TRIGGER, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EXECUTE ON `%s`.* TO '%s'@'%s';" % (target, user, host)) + self.db.sql("GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES, CREATE VIEW, EVENT, TRIGGER, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EXECUTE, LOCK TABLES ON `%s`.* TO '%s'@'%s';" % (target, user, host)) else: self.db.sql("GRANT ALL PRIVILEGES ON `%s`.* TO '%s'@'%s';" % (target, user, host)) diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py index e806e8e415..4bbecd2a2e 100644 --- a/frappe/database/mariadb/schema.py +++ b/frappe/database/mariadb/schema.py @@ -82,5 +82,7 @@ class MariaDBTable(DBTable): fieldname = str(e).split("'")[-2] frappe.throw(_("{0} field cannot be set as unique in {1}, as there are non-unique existing values").format( fieldname, self.table_name)) + elif e.args[0]==1067: + frappe.throw(str(e.args[1])) else: raise e diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index 142c103c68..ae9d070976 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -29,31 +29,56 @@ def handle_not_exist(fn): class Workspace: - def __init__(self, page_name): + def __init__(self, page_name, minimal=False): self.page_name = page_name self.extended_cards = [] self.extended_charts = [] self.extended_shortcuts = [] self.user = frappe.get_user() - self.allowed_modules = self.get_cached_value('user_allowed_modules', self.get_allowed_modules) + self.allowed_modules = self.get_cached('user_allowed_modules', self.get_allowed_modules) + self.doc = self.get_page_for_user() if self.doc.module not in self.allowed_modules: raise frappe.PermissionError - self.can_read = self.get_cached_value('user_perm_can_read', self.get_can_read_items) + self.can_read = self.get_cached('user_perm_can_read', self.get_can_read_items) self.allowed_pages = get_allowed_pages(cache=True) self.allowed_reports = get_allowed_reports(cache=True) - self.onboarding_doc = self.get_onboarding_doc() - self.onboarding = None - - self.table_counts = get_table_with_counts() + + if not minimal: + self.onboarding_doc = self.get_onboarding_doc() + self.onboarding = None + + self.table_counts = get_table_with_counts() self.restricted_doctypes = frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache() self.restricted_pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache() - def get_cached_value(self, cache_key, fallback_fn): + def is_page_allowed(self): + cards = self.doc.cards + get_custom_reports_and_doctypes(self.doc.module) + self.extended_cards + shortcuts = self.doc.shortcuts + self.extended_shortcuts + + for section in cards: + links = loads(section.links) if isinstance(section.links, string_types) else section.links + for item in links: + if self.is_item_allowed(item.get('name'), item.get('type')): + return True + + def _in_active_domains(item): + if not item.restrict_to_domain: + return True + else: + return item.restrict_to_domain in frappe.get_active_domains() + + for item in shortcuts: + if self.is_item_allowed(item.link_to, item.type) and _in_active_domains(item): + return True + + return False + + def get_cached(self, cache_key, fallback_fn): _cache = frappe.cache() value = _cache.get_value(cache_key, user=frappe.session.user) @@ -83,12 +108,12 @@ class Workspace: 'extends': self.page_name, 'for_user': frappe.session.user } - pages = frappe.get_list("Desk Page", filters=filters) + pages = frappe.get_all("Desk Page", filters=filters, limit=1) if pages: - return frappe.get_doc("Desk Page", pages[0]) + return frappe.get_cached_doc("Desk Page", pages[0]) self.get_pages_to_extend() - return frappe.get_doc("Desk Page", self.page_name) + return frappe.get_cached_doc("Desk Page", self.page_name) def get_onboarding_doc(self): # Check if onboarding is enabled @@ -123,7 +148,7 @@ class Workspace: 'module': ['in', self.allowed_modules] }) - pages = [frappe.get_doc("Desk Page", page['name']) for page in pages] + pages = [frappe.get_cached_doc("Desk Page", page['name']) for page in pages] for page in pages: self.extended_cards = self.extended_cards + page.cards @@ -170,6 +195,7 @@ class Workspace: 'docs_url': self.onboarding_doc.documentation_url, 'items': self.get_onboarding_steps() } + @handle_not_exist def get_cards(self): cards = self.doc.cards @@ -195,6 +221,8 @@ class Workspace: incomplete_dependencies = [d for d in item.dependencies if not _doctype_contains_a_record(d)] if len(incomplete_dependencies): item.incomplete_dependencies = incomplete_dependencies + else: + item.incomplete_dependencies = "" if item.onboard: # Mark Spotlights for initial @@ -323,25 +351,44 @@ def get_desktop_page(page): } @frappe.whitelist() -def get_desk_sidebar_items(flatten=False): +def get_desk_sidebar_items(flatten=False, cache=True): """Get list of sidebar items for desk """ - # don't get domain restricted pages - blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules() + pages = [] + _cache = frappe.cache() + if cache: + pages = _cache.get_value("desk_sidebar_items", user=frappe.session.user) + + if not pages or not cache: + # don't get domain restricted pages + blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules() - filters = { - 'restrict_to_domain': ['in', frappe.get_active_domains()], - 'extends_another_page': 0, - 'for_user': '', - 'module': ['not in', blocked_modules] - } + filters = { + 'restrict_to_domain': ['in', frappe.get_active_domains()], + 'extends_another_page': 0, + 'for_user': '', + 'module': ['not in', blocked_modules] + } - if not frappe.local.conf.developer_mode: - filters['developer_mode_only'] = '0' + if not frappe.local.conf.developer_mode: + filters['developer_mode_only'] = '0' + + # pages sorted based on pinned to top and then by name + order_by = "pin_to_top desc, pin_to_bottom asc, name asc" + all_pages = frappe.get_all("Desk Page", fields=["name", "category"], filters=filters, order_by=order_by, ignore_permissions=True) + pages = [] + + # Filter Page based on Permission + for page in all_pages: + try: + wspace = Workspace(page.get('name'), True) + if wspace.is_page_allowed(): + pages.append(page) + except frappe.PermissionError: + pass + + _cache.set_value("desk_sidebar_items", pages, frappe.session.user) - # pages sorted based on pinned to top and then by name - order_by = "pin_to_top desc, pin_to_bottom asc, name asc" - pages = frappe.get_all("Desk Page", fields=["name", "category"], filters=filters, order_by=order_by, ignore_permissions=True) if flatten: return pages @@ -375,7 +422,7 @@ def get_custom_reports_and_doctypes(module): ] def get_custom_doctype_list(module): - doctypes = frappe.get_list("DocType", fields=["name"], filters={"custom": 1, "istable": 0, "module": module}, order_by="name", ignore_permissions=True) + doctypes = frappe.get_all("DocType", fields=["name"], filters={"custom": 1, "istable": 0, "module": module}, order_by="name") out = [] for d in doctypes: @@ -390,9 +437,9 @@ def get_custom_doctype_list(module): def get_custom_report_list(module): """Returns list on new style reports for modules.""" - reports = frappe.get_list("Report", fields=["name", "ref_doctype", "report_type"], filters= + reports = frappe.get_all("Report", fields=["name", "ref_doctype", "report_type"], filters= {"is_standard": "No", "disabled": 0, "module": module}, - order_by="name", ignore_permissions=True) + order_by="name") out = [] for r in reports: diff --git a/frappe/desk/doctype/dashboard/dashboard.js b/frappe/desk/doctype/dashboard/dashboard.js index 609e943995..61300b920b 100644 --- a/frappe/desk/doctype/dashboard/dashboard.js +++ b/frappe/desk/doctype/dashboard/dashboard.js @@ -5,10 +5,14 @@ frappe.ui.form.on('Dashboard', { refresh: function(frm) { frm.add_custom_button(__("Show Dashboard"), () => frappe.set_route('dashboard', frm.doc.name)); + if (!frappe.boot.developer_mode && frm.doc.is_standard) { + frm.disable_form(); + } + frm.set_query("chart", "charts", function() { return { filters: { - is_public: 1 + is_public: 1, } }; }); @@ -16,7 +20,7 @@ frappe.ui.form.on('Dashboard', { frm.set_query("card", "cards", function() { return { filters: { - is_public: 1 + is_public: 1, } }; }); diff --git a/frappe/desk/doctype/dashboard/dashboard.json b/frappe/desk/doctype/dashboard/dashboard.json index c0e2bddcf8..95b0846452 100644 --- a/frappe/desk/doctype/dashboard/dashboard.json +++ b/frappe/desk/doctype/dashboard/dashboard.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_rename": 1, "autoname": "field:dashboard_name", "creation": "2019-01-10 12:54:40.938705", "doctype": "DocType", @@ -8,6 +9,8 @@ "field_order": [ "dashboard_name", "is_default", + "is_standard", + "module", "charts", "chart_options", "cards" @@ -35,21 +38,36 @@ "reqd": 1 }, { - "description": "Set Default Options for all charts on this Dashboard (Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"])", - "fieldname": "chart_options", - "fieldtype": "Code", - "label": "Chart Options", - "options": "JSON" + "description": "Set Default Options for all charts on this Dashboard (Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"])", + "fieldname": "chart_options", + "fieldtype": "Code", + "label": "Chart Options", + "options": "JSON" }, { "fieldname": "cards", "fieldtype": "Table", "label": "Cards", "options": "Number Card Link" + }, + { + "default": "0", + "fieldname": "is_standard", + "fieldtype": "Check", + "label": "Is Standard", + "read_only_depends_on": "eval: !frappe.boot.developer_mode" + }, + { + "depends_on": "eval: doc.is_standard", + "fieldname": "module", + "fieldtype": "Link", + "label": "Module", + "mandatory_depends_on": "eval: doc.is_standard", + "options": "Module Def" } ], "links": [], - "modified": "2020-04-29 13:26:37.362482", + "modified": "2020-07-23 11:05:41.890459", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard", diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py index af0c48d9c6..b12bcfe27d 100644 --- a/frappe/desk/doctype/dashboard/dashboard.py +++ b/frappe/desk/doctype/dashboard/dashboard.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals from frappe.model.document import Document +from frappe.modules.export_file import export_to_files import frappe from frappe import _ import json @@ -15,7 +16,23 @@ class Dashboard(Document): frappe.db.sql('''update tabDashboard set is_default = 0 where name != %s''', self.name) + if frappe.conf.developer_mode and self.is_standard: + export_to_files(record_list=[['Dashboard', self.name, self.module + ' Dashboard']], record_module=self.module) + def validate(self): + if not frappe.conf.developer_mode and self.is_standard: + frappe.throw('Cannot edit Standard Dashboards') + + if self.is_standard: + non_standard_docs_map = { + 'Dashboard Chart': get_non_standard_charts_in_dashboard(self), + 'Number Card': get_non_standard_cards_in_dashboard(self) + } + + if non_standard_docs_map['Dashboard Chart'] or non_standard_docs_map['Number Card']: + message = get_non_standard_warning_message(non_standard_docs_map) + frappe.throw(message, title=_("Standard Not Set"), is_minimizable=True) + self.validate_custom_options() def validate_custom_options(self): @@ -48,3 +65,29 @@ def get_permitted_cards(dashboard_name): if frappe.has_permission('Number Card', doc=card.card): permitted_cards.append(card) return permitted_cards + +def get_non_standard_charts_in_dashboard(dashboard): + non_standard_charts = [doc.name for doc in frappe.get_list('Dashboard Chart', {'is_standard': 0})] + return [chart_link.chart for chart_link in dashboard.charts if chart_link.chart in non_standard_charts] + +def get_non_standard_cards_in_dashboard(dashboard): + non_standard_cards = [doc.name for doc in frappe.get_list('Number Card', {'is_standard': 0})] + return [card_link.card for card_link in dashboard.cards if card_link.card in non_standard_cards] + +def get_non_standard_warning_message(non_standard_docs_map): + message = _('''Please set the following documents in this Dashboard as standard first.''') + + def get_html(docs, doctype): + html = '

{}

'.format(frappe.bold(doctype)) + for doc in docs: + html += '
{doc}
'.format(doctype=doctype, doc=doc) + html += '
' + return html + + html = message + '
' + + for doctype in non_standard_docs_map: + if non_standard_docs_map[doctype]: + html += get_html(non_standard_docs_map[doctype], doctype) + + return html diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js index a10d3d96f2..8d89cc2f31 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js @@ -9,46 +9,41 @@ frappe.ui.form.on('Dashboard Chart', { frm.add_fetch('source', 'timeseries', 'timeseries'); }, + before_save: function(frm) { + let dynamic_filters = JSON.parse(frm.doc.dynamic_filters_json || 'null'); + let static_filters = JSON.parse(frm.doc.filters_json || 'null'); + static_filters = + frappe.dashboard_utils.remove_common_static_filter_values(static_filters, dynamic_filters); + + frm.set_value('filters_json', JSON.stringify(static_filters)); + frm.trigger('show_filters'); + }, refresh: function(frm) { frm.chart_filters = null; + + if (!frappe.boot.developer_mode && frm.doc.is_standard) { + frm.set_df_property('chart_options_section', 'hidden', 1); + frm.disable_form(); + } + frm.add_custom_button('Add Chart to Dashboard', () => { - const d = new frappe.ui.Dialog({ - title: __('Add to Dashboard'), - fields: [ - { - label: __('Select Dashboard'), - fieldtype: 'Link', - fieldname: 'dashboard', - options: 'Dashboard', - } - ], - primary_action: (values) => { - values.chart_name = frm.doc.chart_name; - frappe.xcall( - 'frappe.desk.doctype.dashboard_chart.dashboard_chart.add_chart_to_dashboard', - {args: values} - ).then(()=> { - let dashboard_route_html = - `${values.dashboard}`; - let message = - __(`Dashboard Chart ${values.chart_name} add to Dashboard ` + dashboard_route_html); - - frappe.msgprint(message); - }); - - d.hide(); - } - }); + const dialog = frappe.dashboard_utils.get_add_to_dashboard_dialog( + frm.doc.name, + 'Dashboard Chart', + 'frappe.desk.doctype.dashboard_chart.dashboard_chart.add_chart_to_dashboard' + ); if (!frm.doc.chart_name) { frappe.msgprint(__('Please create chart first')); } else { - d.show(); + dialog.show(); } }); frm.set_df_property("filters_section", "hidden", 1); + frm.set_df_property("dynamic_filters_section", "hidden", 1); + frm.trigger('set_time_series'); frm.set_query('document_type', function() { return { @@ -66,6 +61,15 @@ frappe.ui.form.on('Dashboard Chart', { if (!frappe.boot.developer_mode) { frm.set_df_property("custom_options", "hidden", 1); } + + }, + + is_standard: function(frm) { + if (frappe.boot.developer_mode && frm.doc.is_standard) { + frm.trigger('render_dynamic_filters_table'); + } else { + frm.set_df_property("dynamic_filters_section", "hidden", 1); + } }, source: function(frm) { @@ -111,6 +115,7 @@ frappe.ui.form.on('Dashboard Chart', { frm.set_value('based_on', ''); frm.set_value('value_based_on', ''); frm.set_value('filters_json', '[]'); + frm.set_value('dynamic_filters_json', '[]'); frm.trigger('update_options'); }, @@ -119,6 +124,8 @@ frappe.ui.form.on('Dashboard Chart', { frm.set_value('y_axis', []); frm.set_df_property('x_field', 'options', []); frm.set_value('filters_json', '{}'); + frm.set_value('dynamic_filters_json', '{}'); + frm.set_value('use_report_chart', 0); frm.trigger('set_chart_report_filters'); }, @@ -146,7 +153,10 @@ frappe.ui.form.on('Dashboard Chart', { }, set_chart_field_options: function(frm) { - let filters = frm.doc.filters_json.length > 2? JSON.parse(frm.doc.filters_json): null; + let filters = frm.doc.filters_json.length > 2 ? JSON.parse(frm.doc.filters_json) : null; + if (frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2) { + filters = frappe.dashboard_utils.get_all_filters(frm.doc); + } frappe.xcall( 'frappe.desk.query_report.run', { @@ -156,16 +166,13 @@ frappe.ui.form.on('Dashboard Chart', { } ).then(data => { frm.report_data = data; - if (!data.chart) { - frm.set_value('is_custom', 0); - frm.set_df_property('is_custom', 'hidden', 1); - } else { - frm.set_df_property('is_custom', 'hidden', 0); - } + let report_has_chart = Boolean(data.chart); - if (!frm.doc.is_custom) { + frm.set_df_property('use_report_chart', 'hidden', !report_has_chart); + + if (!frm.doc.use_report_chart) { if (data.result.length) { - frm.field_options = frappe.report_utils.get_possible_chart_options(data.columns, data); + frm.field_options = frappe.report_utils.get_field_options_from_report(data.columns, data); frm.set_df_property('x_field', 'options', frm.field_options.non_numeric_fields); if (!frm.field_options.numeric_fields.length) { frappe.msgprint(__(`Report has no numeric fields, please change the Report Name`)); @@ -240,11 +247,14 @@ frappe.ui.form.on('Dashboard Chart', { show_filters: function(frm) { frm.chart_filters = []; frappe.dashboard_utils.get_filters_for_chart_type(frm.doc).then(filters => { - if (filters) { - frm.chart_filters = filters; - } + if (filters) { + frm.chart_filters = filters; + } + frm.trigger('render_filters_table'); - frm.trigger('render_filters_table'); + if (frappe.boot.developer_mode && frm.doc.is_standard) { + frm.trigger('render_dynamic_filters_table'); + } }); }, @@ -257,8 +267,8 @@ frappe.ui.form.on('Dashboard Chart', { let table = $(` - - + + @@ -281,7 +291,7 @@ frappe.ui.form.on('Dashboard Chart', { set_filters && frm.set_value('filters_json', JSON.stringify(filters)); } - let fields; + let fields = []; if (is_document_type) { fields = [ { @@ -306,7 +316,7 @@ frappe.ui.form.on('Dashboard Chart', { } else if (frm.chart_filters.length) { fields = frm.chart_filters.filter(f => f.fieldname); - fields.map( f => { + fields.map(f => { if (filters[f.fieldname]) { let condition = '='; const filter_row = @@ -378,4 +388,102 @@ frappe.ui.form.on('Dashboard Chart', { }); }, + render_dynamic_filters_table(frm) { + frm.set_df_property("dynamic_filters_section", "hidden", 0); + + let is_document_type = frm.doc.chart_type !== 'Report' + && frm.doc.chart_type !== 'Custom'; + + let wrapper = $(frm.get_field('dynamic_filters_json').wrapper).empty(); + + frm.dynamic_filter_table = $(`
${__('Filter')}${__('Condition')}${__('Filter')}${__('Condition')} ${__('Value')}
+ + + + + + + + +
${__('Filter')}${__('Condition')}${__('Value')}
`).appendTo(wrapper); + + frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2 + ? JSON.parse(frm.doc.dynamic_filters_json) + : null; + + frm.trigger('set_dynamic_filters_in_table'); + + let filters = JSON.parse(frm.doc.filters_json || '[]'); + + let fields = frappe.dashboard_utils.get_fields_for_dynamic_filter_dialog( + is_document_type, filters, frm.dynamic_filters + ); + + frm.dynamic_filter_table.on('click', () => { + let dialog = new frappe.ui.Dialog({ + title: __('Set Dynamic Filters'), + fields: fields, + primary_action: () => { + let values = dialog.get_values(); + dialog.hide(); + let dynamic_filters = []; + for (let key of Object.keys(values)) { + if (is_document_type) { + let [doctype, fieldname] = key.split(':'); + dynamic_filters.push([doctype, fieldname, '=', values[key]]); + } + } + + if (is_document_type) { + frm.set_value('dynamic_filters_json', JSON.stringify(dynamic_filters)); + } else { + frm.set_value('dynamic_filters_json', JSON.stringify(values)); + } + frm.trigger('set_dynamic_filters_in_table'); + }, + primary_action_label: "Set" + }); + + dialog.show(); + dialog.set_values(frm.dynamic_filters); + }); + }, + + set_dynamic_filters_in_table: function(frm) { + frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2 + ? JSON.parse(frm.doc.dynamic_filters_json) + : null; + + if (!frm.dynamic_filters) { + const filter_row = $(` + ${__("Click to Set Dynamic Filters")}`); + frm.dynamic_filter_table.find('tbody').html(filter_row); + } else { + let filter_rows = ''; + if ($.isArray(frm.dynamic_filters)) { + frm.dynamic_filters.forEach(filter => { + filter_rows += + ` + ${filter[1]} + ${filter[2] || ""} + ${filter[3]} + `; + }); + } else { + let condition = '='; + for (let [key, val] of Object.entries(frm.dynamic_filters)) { + filter_rows += + ` + ${key} + ${condition} + ${val || ""} + ` + ; + } + } + + frm.dynamic_filter_table.find('tbody').html(filter_rows); + } + } + }); diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json index 4bab76337f..d4bba53068 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json @@ -7,10 +7,12 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "is_standard", + "module", "chart_name", "chart_type", "report_name", - "is_custom", + "use_report_chart", "x_field", "y_axis", "source", @@ -32,10 +34,12 @@ "type", "filters_section", "filters_json", + "dynamic_filters_section", + "dynamic_filters_json", "chart_options_section", - "color", - "column_break_2", "custom_options", + "column_break_2", + "color", "section_break_10", "last_synced_on" ], @@ -67,7 +71,8 @@ "fieldname": "document_type", "fieldtype": "Link", "label": "Document Type", - "options": "DocType" + "options": "DocType", + "set_only_once": 1 }, { "depends_on": "eval: doc.timeseries && ['Count', 'Sum', 'Average'].includes(doc.chart_type)", @@ -189,32 +194,27 @@ "label": "To Date" }, { - "depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.is_custom", + "depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.use_report_chart", "fieldname": "x_field", "fieldtype": "Select", "label": "X Field", - "mandatory_depends_on": "eval: doc.report_name && !doc.is_custom" + "mandatory_depends_on": "eval: doc.report_name && !doc.use_report_chart" }, { "depends_on": "eval:doc.chart_type === 'Report'", "fieldname": "report_name", "fieldtype": "Link", "label": "Report Name", - "options": "Report" + "mandatory_depends_on": "eval:doc.chart_type === 'Report'", + "options": "Report", + "set_only_once": 1 }, { - "default": "0", - "depends_on": "eval: doc.report_name", - "fieldname": "is_custom", - "fieldtype": "Check", - "label": "Is Custom" - }, - { - "depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.is_custom", + "depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.use_report_chart", "fieldname": "y_axis", "fieldtype": "Table", "label": "Y Axis", - "mandatory_depends_on": "eval:doc.report_name && !doc.is_custom", + "mandatory_depends_on": "eval:doc.report_name && !doc.use_report_chart", "options": "Dashboard Chart Field" }, { @@ -235,10 +235,43 @@ "fieldname": "heatmap_year", "fieldtype": "Select", "label": "Year" + }, + { + "default": "0", + "fieldname": "is_standard", + "fieldtype": "Check", + "label": "Is Standard", + "read_only_depends_on": "eval: !frappe.boot.developer_mode" + }, + { + "depends_on": "eval: doc.is_standard", + "fieldname": "module", + "fieldtype": "Link", + "label": "Module", + "mandatory_depends_on": "eval: doc.is_standard", + "options": "Module Def" + }, + { + "fieldname": "dynamic_filters_json", + "fieldtype": "Code", + "label": "Dynamic Filters JSON", + "options": "JSON" + }, + { + "fieldname": "dynamic_filters_section", + "fieldtype": "Section Break", + "label": "Dynamic Filters" + }, + { + "default": "0", + "depends_on": "eval: doc.report_name", + "fieldname": "use_report_chart", + "fieldtype": "Check", + "label": "Use Report Chart" } ], "links": [], - "modified": "2020-05-16 15:03:02.455395", + "modified": "2020-07-23 11:10:33.509497", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard Chart", diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index a5c5504db2..88c9623945 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -13,6 +13,7 @@ from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate from frappe.model.naming import append_number_if_name_exists from frappe.boot import get_allowed_reports from frappe.model.document import Document +from frappe.modules.export_file import export_to_files def get_permission_query_conditions(user): @@ -27,15 +28,28 @@ def get_permission_query_conditions(user): if "System Manager" in roles: return None - allowed_doctypes = ['"%s"' % doctype for doctype in frappe.permissions.get_doctypes_with_read()] - allowed_reports = ['"%s"' % key if type(key) == str else key.encode('UTF8') for key in get_allowed_reports()] + doctype_condition = False + report_condition = False + + allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()] + allowed_reports = [frappe.db.escape(key) if type(key) == str else key.encode('UTF8') for key in get_allowed_reports()] + + if allowed_doctypes: + doctype_condition = '`tabDashboard Chart`.`document_type` in ({allowed_doctypes})'.format( + allowed_doctypes=','.join(allowed_doctypes)) + if allowed_reports: + report_condition = '`tabDashboard Chart`.`report_name` in ({allowed_reports})'.format( + allowed_reports=','.join(allowed_reports)) return ''' - `tabDashboard Chart`.`document_type` in ({allowed_doctypes}) - or `tabDashboard Chart`.`report_name` in ({allowed_reports}) + (`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average') + and {doctype_condition}) + or + (`tabDashboard Chart`.`chart_type` = 'Report' + and {report_condition}) '''.format( - allowed_doctypes=','.join(allowed_doctypes), - allowed_reports=','.join(allowed_reports) + doctype_condition=doctype_condition, + report_condition=report_condition ) @@ -80,7 +94,9 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d to_date = get_datetime(chart.to_date) timegrain = time_interval or chart.time_interval - filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json) or [] + filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json) + if not filters: + filters = [] # don't include cancelled documents filters.append([chart.document_type, 'docstatus', '<', 2, False]) @@ -125,7 +141,13 @@ def add_chart_to_dashboard(args): dashboard = frappe.get_doc('Dashboard', args.dashboard) dashboard_link = frappe.new_doc('Dashboard Chart Link') - dashboard_link.chart = args.chart_name + dashboard_link.chart = args.chart_name or args.name + + if args.set_standard: + chart = frappe.get_doc('Dashboard Chart', dashboard_link.chart) + chart.is_standard = 1 + chart.module = dashboard.module + chart.save() dashboard.append('charts', dashboard_link) dashboard.save() @@ -260,9 +282,7 @@ def get_result(data, timegrain, from_date, to_date): start_date = getdate(from_date) end_date = getdate(to_date) - result = [] - if timegrain == 'Daily': - result.append([start_date, 0.0]) + result = [[start_date, 0.0]] while start_date < end_date: next_date = get_next_expected_date(start_date, timegrain) @@ -280,11 +300,8 @@ def get_result(data, timegrain, from_date, to_date): def get_next_expected_date(date, timegrain): next_date = None - if timegrain=='Daily': - next_date = add_to_date(date, days=1) - else: - # given date is always assumed to be the period ending date - next_date = get_period_ending(add_to_date(date, days=1), timegrain) + # given date is always assumed to be the period ending date + next_date = get_period_ending(add_to_date(date, days=1), timegrain) return getdate(next_date) def get_period_ending(date, timegrain): @@ -340,6 +357,7 @@ def get_year_ending(date): # last day of this month return add_to_date(date, days=-1) +@frappe.whitelist() def get_charts_for_user(doctype, txt, searchfield, start, page_len, filters): or_filters = {'owner': frappe.session.user, 'is_public': 1} return frappe.db.get_list('Dashboard Chart', @@ -352,8 +370,13 @@ class DashboardChart(Document): def on_update(self): frappe.cache().delete_key('chart-data:{}'.format(self.name)) + if frappe.conf.developer_mode and self.is_standard: + export_to_files(record_list=[['Dashboard Chart', self.name]], record_module=self.module) + def validate(self): + if not frappe.conf.developer_mode and self.is_standard: + frappe.throw('Cannot edit Standard charts') if self.chart_type != 'Custom' and self.chart_type != 'Report': self.check_required_field() self.check_document_type() diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py index 1a300e471a..5e39998e62 100644 --- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py @@ -4,13 +4,12 @@ from __future__ import unicode_literals import unittest, frappe -from frappe.utils import getdate, formatdate +from frappe.utils import getdate, formatdate, get_last_day from frappe.desk.doctype.dashboard_chart.dashboard_chart import (get, get_period_ending) from datetime import datetime from dateutil.relativedelta import relativedelta -import calendar class TestDashboardChart(unittest.TestCase): def test_period_ending(self): @@ -53,16 +52,18 @@ class TestDashboardChart(unittest.TestCase): cur_date = datetime.now() - relativedelta(years=1) - result = get(chart_name ='Test Dashboard Chart', refresh = 1) - for idx in range(13): - month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1])) + result = get(chart_name='Test Dashboard Chart', refresh=1) + self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d'))) + + if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')): + cur_date += relativedelta(months=1) + + for idx in range(1, 13): + month = get_last_day(cur_date) month = formatdate(month.strftime('%Y-%m-%d')) self.assertEqual(result.get('labels')[idx], month) cur_date += relativedelta(months=1) - # self.assertEqual(result.get('datasets')[0].get('values')[:-1], - # [44, 28, 8, 11, 2, 6, 18, 6, 4, 5, 15, 13]) - frappe.db.rollback() def test_empty_dashboard_chart(self): @@ -79,15 +80,20 @@ class TestDashboardChart(unittest.TestCase): based_on = 'creation', timespan = 'Last Year', time_interval = 'Monthly', - filters_json = '{}', + filters_json = '[]', timeseries = 1 )).insert() cur_date = datetime.now() - relativedelta(years=1) - result = get(chart_name ='Test Empty Dashboard Chart', refresh = 1) - for idx in range(13): - month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1])) + result = get(chart_name ='Test Empty Dashboard Chart', refresh=1) + self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d'))) + + if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')): + cur_date += relativedelta(months=1) + + for idx in range(1, 13): + month = get_last_day(cur_date) month = formatdate(month.strftime('%Y-%m-%d')) self.assertEqual(result.get('labels')[idx], month) cur_date += relativedelta(months=1) @@ -111,15 +117,20 @@ class TestDashboardChart(unittest.TestCase): based_on = 'creation', timespan = 'Last Year', time_interval = 'Monthly', - filters_json = '{}', + filters_json = '[]', timeseries = 1 )).insert() cur_date = datetime.now() - relativedelta(years=1) result = get(chart_name ='Test Empty Dashboard Chart 2', refresh = 1) - for idx in range(13): - month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1])) + self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d'))) + + if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')): + cur_date += relativedelta(months=1) + + for idx in range(1, 13): + month = get_last_day(cur_date) month = formatdate(month.strftime('%Y-%m-%d')) self.assertEqual(result.get('labels')[idx], month) cur_date += relativedelta(months=1) @@ -141,7 +152,7 @@ class TestDashboardChart(unittest.TestCase): chart_type = 'Group By', document_type = 'ToDo', group_by_based_on = 'status', - filters_json = '{}', + filters_json = '[]', )).insert() result = get(chart_name ='Test Group By Dashboard Chart', refresh = 1) @@ -168,7 +179,7 @@ class TestDashboardChart(unittest.TestCase): time_interval = 'Daily', from_date = datetime(2019, 1, 6), to_date = datetime(2019, 1, 11), - filters_json = '{}', + filters_json = '[]', timeseries = 1 )).insert() @@ -200,22 +211,24 @@ class TestDashboardChart(unittest.TestCase): time_interval = 'Weekly', from_date = datetime(2018, 12, 30), to_date = datetime(2019, 1, 15), - filters_json = '{}', + filters_json = '[]', timeseries = 1 )).insert() result = get(chart_name ='Test Weekly Dashboard Chart', refresh = 1) - self.assertEqual(result.get('datasets')[0].get('values'), [200.0, 800.0, 0.0]) - self.assertEqual(result.get('labels'), [formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')]) + self.assertEqual(result.get('datasets')[0].get('values'), [50.0, 300.0, 800.0, 0.0]) + self.assertEqual(result.get('labels'), [formatdate('2018-12-30'), formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')]) frappe.db.rollback() def insert_test_records(): - create_new_communication(datetime(2019, 1, 10), 100) + create_new_communication(datetime(2018, 12, 30), 50) + create_new_communication(datetime(2019, 1, 4), 100) create_new_communication(datetime(2019, 1, 6), 200) create_new_communication(datetime(2019, 1, 7), 400) create_new_communication(datetime(2019, 1, 8), 300) + create_new_communication(datetime(2019, 1, 10), 100) def create_new_communication(date, rating): communication = { diff --git a/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.json b/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.json index 7f6532ce1f..fbe0ae94f0 100644 --- a/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.json +++ b/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.json @@ -1,162 +1,69 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, + "actions": [], "autoname": "field:source_name", - "beta": 0, "creation": "2019-02-06 07:55:29.579840", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "source_name", + "module", + "timeseries" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "source_name", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Source Name", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, "unique": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "module", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Module", - "length": 0, - "no_copy": 0, "options": "Module Def", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "timeseries", "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": "Timeseries", - "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 + "label": "Timeseries" } ], - "has_web_view": 0, - "hide_toolbar": 0, - "idx": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-04-09 14:20:51.548207", + "links": [], + "modified": "2020-06-26 18:00:37.421491", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard Chart Source", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, + "share": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", "share": 1, - "submit": 0, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "title_field": "", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.py b/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.py index de83807b4b..6685009078 100644 --- a/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.py +++ b/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.py @@ -18,10 +18,6 @@ def get_config(name): return f.read() class DashboardChartSource(Document): - def validate(self): - if frappe.session.user != "Administrator": - frappe.throw(_("Only Administrator is allowed to create Dashboard Chart Sources")) - def on_update(self): export_to_files(record_list=[[self.doctype, self.name]], record_module=self.module, create_init=True) diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py index 12f2c41274..c4c6077e85 100644 --- a/frappe/desk/doctype/notification_log/notification_log.py +++ b/frappe/desk/doctype/notification_log/notification_log.py @@ -69,7 +69,6 @@ def make_notification_logs(doc, users): _doc = frappe.new_doc('Notification Log') _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' or doc.type == 'Alert': _doc.insert(ignore_permissions=True) diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js index 184fe5e6cb..63b41b956e 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -3,8 +3,130 @@ frappe.ui.form.on('Number Card', { refresh: function(frm) { + if (!frappe.boot.developer_mode && frm.doc.is_standard) { + frm.disable_form(); + } frm.set_df_property("filters_section", "hidden", 1); + frm.set_df_property("dynamic_filters_section", "hidden", 1); frm.trigger('set_options'); + + if (!frm.doc.type) { + frm.set_value('type', 'Document Type'); + } + + if (frm.doc.type == 'Report' && frm.doc.report_name) { + frm.trigger('set_report_filters'); + } + + if (frm.doc.type == 'Custom') { + if (!frappe.boot.developer_mode) { + frm.disable_form(); + } + frm.filters = eval(frm.doc.filters_config); + frm.trigger('set_filters_description'); + frm.trigger('set_method_description'); + frm.trigger('render_filters_table'); + } + frm.trigger('create_add_to_dashboard_button'); + }, + + create_add_to_dashboard_button: function(frm) { + frm.add_custom_button('Add Card to Dashboard', () => { + const dialog = frappe.dashboard_utils.get_add_to_dashboard_dialog( + frm.doc.name, + 'Number Card', + 'frappe.desk.doctype.number_card.number_card.add_card_to_dashboard' + ); + + if (!frm.doc.name) { + frappe.msgprint(__('Please create Card first')); + } else { + dialog.show(); + } + }); + }, + + before_save: function(frm) { + let dynamic_filters = JSON.parse(frm.doc.dynamic_filters_json || 'null'); + let static_filters = JSON.parse(frm.doc.filters_json || 'null'); + static_filters = + frappe.dashboard_utils.remove_common_static_filter_values(static_filters, dynamic_filters); + + frm.set_value('filters_json', JSON.stringify(static_filters)); + frm.trigger('render_filters_table'); + frm.trigger('render_dynamic_filters_table'); + }, + + is_standard: function(frm) { + frm.trigger('render_dynamic_filters_table'); + frm.set_df_property("dynamic_filters_section", "hidden", 1); + }, + + set_filters_description: function(frm) { + if (frm.doc.type == 'Custom') { + frm.fields_dict.filters_config.set_description(` + Set the filters here. For example: +
+
+[{
+	fieldname: "company",
+	label: __("Company"),
+	fieldtype: "Link",
+	options: "Company",
+	default: frappe.defaults.get_user_default("Company"),
+	reqd: 1
+},
+{
+	fieldname: "account",
+	label: __("Account"),
+	fieldtype: "Link",
+	options: "Account",
+	reqd: 1
+}]
+
`); + } + }, + + set_method_description: function(frm) { + if (frm.doc.type == 'Custom') { + frm.fields_dict.method.set_description(` + Set the path to a whitelisted function that will return the number on the card in the format: +
+
+{
+	"value": value,
+	"fieldtype": "Currency"
+}
+
`); + } + }, + + type: function(frm) { + frm.trigger('set_filters_description'); + if (frm.doc.type == 'Report') { + frm.set_query('report_name', () => { + return { + filters: { + 'report_type': ['!=', 'Report Builder'] + } + }; + }); + } + + }, + + report_name: function(frm) { + frm.filters = []; + frm.set_value('filters_json', '{}'); + frm.set_value('dynamic_filters_json', '{}'); + frm.set_df_property('report_field', 'options', []); + frm.trigger('set_report_filters'); + }, + + filters_config: function(frm) { + frm.filters = eval(frm.doc.filters_config); + const filter_values = frappe.report_utils.get_filter_values(frm.filters); + frm.set_value('filters_json', JSON.stringify(filter_values)); frm.trigger('render_filters_table'); }, @@ -17,11 +139,16 @@ frappe.ui.form.on('Number Card', { }; }); frm.set_value('filters_json', '[]'); + frm.set_value('dynamic_filters_json', '[]'); frm.set_value('aggregate_function_based_on', ''); frm.trigger('set_options'); }, set_options: function(frm) { + if (frm.doc.type !== 'Document Type') { + return; + } + let aggregate_based_on_fields = []; const doctype = frm.doc.document_type; @@ -40,80 +167,275 @@ frappe.ui.form.on('Number Card', { frm.set_df_property('aggregate_function_based_on', 'options', aggregate_based_on_fields); }); + frm.trigger('render_filters_table'); + frm.trigger('render_dynamic_filters_table'); } }, + set_report_filters: function(frm) { + const report_name = frm.doc.report_name; + if (report_name) { + frappe.report_utils.get_report_filters(report_name).then(filters => { + if (filters) { + frm.filters = filters; + const filter_values = frappe.report_utils.get_filter_values(filters); + if (frm.doc.filters_json.length <= 2) { + frm.set_value('filters_json', JSON.stringify(filter_values)); + } + } + frm.trigger('render_filters_table'); + frm.trigger('set_report_field_options'); + frm.trigger('render_dynamic_filters_table'); + }); + } + }, + + set_report_field_options: function(frm) { + let filters = frm.doc.filters_json.length > 2 ? JSON.parse(frm.doc.filters_json) : null; + if (frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2) { + filters = frappe.dashboard_utils.get_all_filters(frm.doc); + } + frappe.xcall( + 'frappe.desk.query_report.run', + { + report_name: frm.doc.report_name, + filters: filters, + ignore_prepared_report: 1 + } + ).then(data => { + if (data.result.length) { + frm.field_options = frappe.report_utils.get_field_options_from_report(data.columns, data); + frm.set_df_property('report_field', 'options', frm.field_options.numeric_fields); + if (!frm.field_options.numeric_fields.length) { + frappe.msgprint(__(`Report has no numeric fields, please change the Report Name`)); + } + } else { + frappe.msgprint(__('Report has no data, please modify the filters or change the Report Name')); + } + }); + }, + render_filters_table: function(frm) { frm.set_df_property("filters_section", "hidden", 0); + let is_document_type = frm.doc.type == 'Document Type'; + let is_dynamic_filter = f => ['Date', 'DateRange'].includes(f.fieldtype) && f.default; let wrapper = $(frm.get_field('filters_json').wrapper).empty(); - frm.filter_table = $(` + let table = $(`
- - + + + + + + +
${__('Filter')}${__('Condition')}${__('Filter')}${__('Condition')}${__('Value')}
`).appendTo(wrapper); + $(`

${__("Click table to edit")}

`).appendTo(wrapper); + + let filters = JSON.parse(frm.doc.filters_json || '[]'); + let filters_set = false; + + // Set dynamic filters for reports + if (frm.doc.type == 'Report') { + let set_filters = false; + frm.filters.forEach(f => { + if (is_dynamic_filter(f)) { + filters[f.fieldname] = f.default; + set_filters = true; + } + }); + set_filters && frm.set_value('filters_json', JSON.stringify(filters)); + } + + let fields = []; + if (is_document_type) { + fields = [ + { + fieldtype: 'HTML', + fieldname: 'filter_area', + } + ]; + + if (filters.length) { + filters.forEach(filter => { + const filter_row = + $(` + ${filter[1]} + ${filter[2] || ""} + ${filter[3]} + `); + + table.find('tbody').append(filter_row); + }); + filters_set = true; + } + } else if (frm.filters.length) { + fields = frm.filters.filter(f => f.fieldname); + fields.map(f => { + if (filters[f.fieldname]) { + let condition = '='; + const filter_row = + $(` + ${f.label} + ${condition} + ${filters[f.fieldname] || ""} + `); + table.find('tbody').append(filter_row); + if (!filters_set) filters_set = true; + } + }); + } + + if (!filters_set) { + const filter_row = $(` + ${__("Click to Set Filters")}`); + table.find('tbody').append(filter_row); + } + + table.on('click', () => { + let dialog = new frappe.ui.Dialog({ + title: __('Set Filters'), + fields: fields.filter(f => !is_dynamic_filter(f)), + primary_action: function() { + let values = this.get_values(); + if (values) { + this.hide(); + if (is_document_type) { + let filters = frm.filter_group.get_filters(); + frm.set_value('filters_json', JSON.stringify(filters)); + } else { + frm.set_value('filters_json', JSON.stringify(values)); + } + frm.trigger('render_filters_table'); + } + }, + primary_action_label: "Set" + }); + + if (is_document_type) { + frm.filter_group = new frappe.ui.FilterGroup({ + parent: dialog.get_field('filter_area').$wrapper, + doctype: frm.doc.document_type, + on_change: () => {}, + }); + filters && frm.filter_group.add_filters_to_filter_group(filters); + } + + dialog.show(); + + if (frm.doc.type == 'Report') { + //Set query report object so that it can be used while fetching filter values in the report + frappe.query_report = new frappe.views.QueryReport({'filters': dialog.fields_list}); + frappe.query_reports[frm.doc.report_name] + && frappe.query_reports[frm.doc.report_name].onload + && frappe.query_reports[frm.doc.report_name].onload(frappe.query_report); + } + + dialog.set_values(filters); + }); + + }, + + render_dynamic_filters_table(frm) { + if (!frappe.boot.developer_mode || !frm.doc.is_standard || frm.doc.type == 'Custom') { + return; + } + + frm.set_df_property("dynamic_filters_section", "hidden", 0); + + let is_document_type = frm.doc.type == 'Document Type'; + + let wrapper = $(frm.get_field('dynamic_filters_json').wrapper).empty(); + + frm.dynamic_filter_table = $(` + + + +
${__('Filter')}${__('Condition')} ${__('Value')}
`).appendTo(wrapper); - frm.filters = JSON.parse(frm.doc.filters_json || '[]'); + frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2 + ? JSON.parse(frm.doc.dynamic_filters_json) + : null; - frm.trigger('set_filters_in_table'); + frm.trigger('set_dynamic_filters_in_table'); - frm.filter_table.on('click', () => { + let filters = JSON.parse(frm.doc.filters_json || '[]'); + + let fields = frappe.dashboard_utils.get_fields_for_dynamic_filter_dialog( + is_document_type, filters, frm.dynamic_filters + ); + + frm.dynamic_filter_table.on('click', () => { let dialog = new frappe.ui.Dialog({ - title: __('Set Filters'), - fields: [{ - fieldtype: 'HTML', - fieldname: 'filter_area', - }], - primary_action: function() { - let values = this.get_values(); - if (values) { - this.hide(); - frm.filters = frm.filter_group.get_filters(); - frm.set_value('filters_json', JSON.stringify(frm.filters)); - frm.trigger('set_filters_in_table'); + title: __('Set Dynamic Filters'), + fields: fields, + primary_action: () => { + let values = dialog.get_values(); + dialog.hide(); + let dynamic_filters = []; + for (let key of Object.keys(values)) { + if (is_document_type) { + let [doctype, fieldname] = key.split(':'); + dynamic_filters.push([doctype, fieldname, '=', values[key]]); + } } + + if (is_document_type) { + frm.set_value('dynamic_filters_json', JSON.stringify(dynamic_filters)); + } else { + frm.set_value('dynamic_filters_json', JSON.stringify(values)); + } + frm.trigger('set_dynamic_filters_in_table'); }, primary_action_label: "Set" }); - frappe.dashboards.filters_dialog = dialog; - - frm.filter_group = new frappe.ui.FilterGroup({ - parent: dialog.get_field('filter_area').$wrapper, - doctype: frm.doc.document_type, - on_change: () => {}, - }); - - frm.filter_group.add_filters_to_filter_group(frm.filters); - dialog.show(); - dialog.set_values(frm.filters); + dialog.set_values(frm.dynamic_filters); }); - }, - set_filters_in_table: function(frm) { - if (!frm.filters.length) { + set_dynamic_filters_in_table: function(frm) { + frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2 + ? JSON.parse(frm.doc.dynamic_filters_json) + : null; + + if (!frm.dynamic_filters) { const filter_row = $(` - ${__("Click to Set Filters")}`); - frm.filter_table.find('tbody').html(filter_row); + ${__("Click to Set Dynamic Filters")}`); + frm.dynamic_filter_table.find('tbody').html(filter_row); } else { let filter_rows = ''; - frm.filters.forEach(filter => { - filter_rows += - ` - ${filter[1]} - ${filter[2] || ""} - ${filter[3]} - `; + if ($.isArray(frm.dynamic_filters)) { + frm.dynamic_filters.forEach(filter => { + filter_rows += + ` + ${filter[1]} + ${filter[2] || ""} + ${filter[3]} + `; + }); + } else { + let condition = '='; + for (let [key, val] of Object.entries(frm.dynamic_filters)) { + filter_rows += + ` + ${key} + ${condition} + ${val || ""} + ` + ; + } + } - }); - frm.filter_table.find('tbody').html(filter_rows); + frm.dynamic_filter_table.find('tbody').html(filter_rows); } } + }); diff --git a/frappe/desk/doctype/number_card/number_card.json b/frappe/desk/doctype/number_card/number_card.json index ec6a1e9190..d3e9598eb7 100644 --- a/frappe/desk/doctype/number_card/number_card.json +++ b/frappe/desk/doctype/number_card/number_card.json @@ -1,39 +1,53 @@ { "actions": [], + "allow_rename": 1, "creation": "2020-04-15 18:06:39.444683", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "is_standard", + "module", "label", + "type", + "report_name", + "method", "function", "aggregate_function_based_on", "column_break_2", "document_type", + "report_field", + "report_function", "is_public", + "custom_configuration_section", + "filters_config", "stats_section", "show_percentage_stats", "stats_time_interval", "filters_section", "filters_json", + "dynamic_filters_section", + "dynamic_filters_json", + "section_break_16", "color" ], "fields": [ { + "depends_on": "eval: doc.type == 'Document Type'", "fieldname": "document_type", "fieldtype": "Link", "in_list_view": 1, "label": "Document Type", - "options": "DocType", - "reqd": 1 + "mandatory_depends_on": "eval: doc.type == 'Document Type'", + "options": "DocType" }, { - "depends_on": "eval: doc.document_type", + "depends_on": "eval: doc.type == 'Document Type'", "fieldname": "function", "fieldtype": "Select", "label": "Function", - "options": "Count\nSum\nAverage\nMinimum\nMaximum", - "reqd": 1 + "mandatory_depends_on": "eval: doc.type == 'Document Type'", + "options": "Count\nSum\nAverage\nMinimum\nMaximum" }, { "depends_on": "eval: doc.function !== 'Count'", @@ -92,13 +106,92 @@ "options": "Daily\nWeekly\nMonthly\nYearly" }, { + "depends_on": "eval: doc.type == 'Document Type'", "fieldname": "stats_section", "fieldtype": "Section Break", "label": "Stats" + }, + { + "default": "0", + "fieldname": "is_standard", + "fieldtype": "Check", + "label": "Is Standard", + "read_only_depends_on": "eval: !frappe.boot.developer_mode" + }, + { + "depends_on": "eval: doc.is_standard", + "fieldname": "module", + "fieldtype": "Link", + "label": "Module", + "mandatory_depends_on": "eval: doc.is_standard", + "options": "Module Def" + }, + { + "fieldname": "dynamic_filters_json", + "fieldtype": "Code", + "label": "Dynamic Filters JSON", + "options": "JSON" + }, + { + "fieldname": "section_break_16", + "fieldtype": "Section Break" + }, + { + "fieldname": "dynamic_filters_section", + "fieldtype": "Section Break", + "label": "Dynamic Filters Section" + }, + { + "fieldname": "type", + "fieldtype": "Select", + "label": "Type", + "options": "Document Type\nReport\nCustom" + }, + { + "depends_on": "eval: doc.type == 'Report'", + "fieldname": "report_name", + "fieldtype": "Link", + "label": "Report Name", + "mandatory_depends_on": "eval: doc.type == 'Report'", + "options": "Report" + }, + { + "depends_on": "eval: doc.type == 'Report'", + "fieldname": "report_field", + "fieldtype": "Select", + "label": "Field", + "mandatory_depends_on": "eval: doc.type == 'Report'" + }, + { + "depends_on": "eval: doc.type == 'Custom'", + "fieldname": "method", + "fieldtype": "Data", + "label": "Method", + "mandatory_depends_on": "eval: doc.type == 'Custom'" + }, + { + "depends_on": "eval: doc.type == 'Custom'", + "fieldname": "custom_configuration_section", + "fieldtype": "Section Break", + "label": "Custom Configuration" + }, + { + "fieldname": "filters_config", + "fieldtype": "Code", + "label": "Filters Configuration", + "options": "JSON" + }, + { + "depends_on": "eval: doc.type == 'Report'", + "fieldname": "report_function", + "fieldtype": "Select", + "label": "Function", + "mandatory_depends_on": "eval: doc.type == 'Report'", + "options": "Sum\nAverage\nMinimum\nMaximum" } ], "links": [], - "modified": "2020-05-06 19:47:57.753574", + "modified": "2020-07-23 11:11:03.391719", "modified_by": "Administrator", "module": "Desk", "name": "Number Card", diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py index c4a427c4e0..68ed79e64b 100644 --- a/frappe/desk/doctype/number_card/number_card.py +++ b/frappe/desk/doctype/number_card/number_card.py @@ -7,6 +7,7 @@ import frappe from frappe.model.document import Document from frappe.utils import cint from frappe.model.naming import append_number_if_name_exists +from frappe.modules.export_file import export_to_files class NumberCard(Document): def autoname(self): @@ -16,6 +17,10 @@ class NumberCard(Document): if frappe.db.exists("Number Card", self.name): self.name = append_number_if_name_exists('Number Card', self.name) + def on_update(self): + if frappe.conf.developer_mode and self.is_standard: + export_to_files(record_list=[['Number Card', self.name]], record_module=self.module) + def get_permission_query_conditions(user=None): if not user: user = frappe.session.user @@ -27,13 +32,17 @@ def get_permission_query_conditions(user=None): if "System Manager" in roles: return None - allowed_doctypes = ['"%s"' % doctype for doctype in frappe.permissions.get_doctypes_with_read()] + doctype_condition = False + + allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()] + + if allowed_doctypes: + doctype_condition = '`tabNumber Card`.`document_type` in ({allowed_doctypes})'.format( + allowed_doctypes=','.join(allowed_doctypes)) return ''' - `tabNumber Card`.`document_type` in ({allowed_doctypes}) - '''.format( - allowed_doctypes=','.join(allowed_doctypes) - ) + {doctype_condition} + '''.format(doctype_condition=doctype_condition) def has_permission(doc, ptype, user): roles = frappe.get_roles(user) @@ -47,7 +56,7 @@ def has_permission(doc, ptype, user): return False @frappe.whitelist() -def get_result(doc, to_date=None): +def get_result(doc, filters, to_date=None): doc = frappe.parse_json(doc) fields = [] sql_function_map = { @@ -65,10 +74,13 @@ def get_result(doc, to_date=None): else: fields = ['{function}({based_on}) as result'.format(function=function, based_on=doc.aggregate_function_based_on)] - filters = frappe.parse_json(doc.filters_json) + filters = frappe.parse_json(filters) + + if not filters: + filters = [] if to_date: - filters.append([doc.document_type, 'creation', '<', to_date, False]) + filters.append([doc.document_type, 'creation', '<', to_date]) res = frappe.db.get_list(doc.document_type, fields=fields, filters=filters) number = res[0]['result'] if res else 0 @@ -76,7 +88,7 @@ def get_result(doc, to_date=None): return cint(number) @frappe.whitelist() -def get_percentage_difference(doc, result): +def get_percentage_difference(doc, filters, result): doc = frappe.parse_json(doc) result = frappe.parse_json(result) @@ -85,13 +97,13 @@ def get_percentage_difference(doc, result): if not doc.get('show_percentage_stats'): return - previous_result = calculate_previous_result(doc) + previous_result = calculate_previous_result(doc, filters) difference = (result - previous_result)/100.0 return difference -def calculate_previous_result(doc): +def calculate_previous_result(doc, filters): from frappe.utils import add_to_date current_date = frappe.utils.now() @@ -104,7 +116,7 @@ def calculate_previous_result(doc): else: previous_date = add_to_date(current_date, years=-1) - number = get_result(doc, previous_date) + number = get_result(doc, filters, previous_date) return number @frappe.whitelist() @@ -116,11 +128,15 @@ def create_number_card(args): doc.insert(ignore_permissions=True) return doc +@frappe.whitelist() def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters): meta = frappe.get_meta(doctype) searchfields = meta.get_search_fields() search_conditions = [] + if not frappe.db.exists('DocType', doctype): + return + if txt: for field in searchfields: search_conditions.append('`tab{doctype}`.`{field}` like %(txt)s'.format(field=field, doctype=doctype, txt=txt)) @@ -147,3 +163,28 @@ def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters): search_conditions=search_conditions, conditions=conditions ), values) + +@frappe.whitelist() +def create_report_number_card(args): + card = create_number_card(args) + args = frappe.parse_json(args) + args.name = card.name + if args.dashboard: + add_card_to_dashboard(frappe.as_json(args)) + +@frappe.whitelist() +def add_card_to_dashboard(args): + args = frappe.parse_json(args) + + dashboard = frappe.get_doc('Dashboard', args.dashboard) + dashboard_link = frappe.new_doc('Number Card Link') + dashboard_link.card = args.name + + if args.set_standard: + card = frappe.get_doc('Number Card', dashboard_link.card) + card.is_standard = 1 + card.module = dashboard.module + card.save() + + dashboard.append('cards', dashboard_link) + dashboard.save() \ No newline at end of file diff --git a/frappe/desk/doctype/tag/tag.json b/frappe/desk/doctype/tag/tag.json index 895516594e..ad9838d10f 100644 --- a/frappe/desk/doctype/tag/tag.json +++ b/frappe/desk/doctype/tag/tag.json @@ -1,4 +1,5 @@ { + "allow_rename": 1, "autoname": "Prompt", "creation": "2016-05-25 09:43:44.767581", "doctype": "DocType", @@ -46,4 +47,4 @@ ], "sort_field": "modified", "sort_order": "DESC" -} \ No newline at end of file +} diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py index a121e71dc8..5bae49ea95 100644 --- a/frappe/desk/form/linked_with.py +++ b/frappe/desk/form/linked_with.py @@ -13,7 +13,7 @@ from frappe.modules import load_doctype_module @frappe.whitelist() -def get_submitted_linked_docs(doctype, name, docs=None, linked=None, visited=None): +def get_submitted_linked_docs(doctype, name, docs=None, visited=None): """ Get all nested submitted linked doctype linkinfo @@ -31,34 +31,27 @@ def get_submitted_linked_docs(doctype, name, docs=None, linked=None, visited=Non if not docs: docs = [] - if not linked: - linked = {} - if not visited: - visited = [] + visited = {} - if name in visited: + if doctype not in visited: + visited[doctype] = [] + + if name in visited[doctype]: return linkinfo = get_linked_doctypes(doctype) linked_docs = get_linked_docs(doctype, name, linkinfo) link_count = 0 - visited.append(name) - + visited[doctype].append(name) + for link_doctype, link_names in linked_docs.items(): - if link_doctype not in linked: - linked[link_doctype] = [] for link in link_names: if link['name'] == name: continue - if linked and name in linked[link_doctype]: - continue - - linked[link_doctype].append(link['name']) - docinfo = link.update({"doctype": link_doctype}) validated_doc = validate_linked_doc(docinfo) @@ -66,10 +59,8 @@ def get_submitted_linked_docs(doctype, name, docs=None, linked=None, visited=Non continue link_count += 1 - if link.name in [doc.get("name") for doc in docs]: - continue - links = get_submitted_linked_docs(link_doctype, link.name, docs, linked, visited) + links = get_submitted_linked_docs(link_doctype, link.name, docs, visited) if links: docs.append({ "doctype": link_doctype, diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index f24f33df07..cacbd3c633 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -100,6 +100,7 @@ def get_docinfo(doc=None, doctype=None, name=None): "shared": frappe.share.get_users(doc.doctype, doc.name), "views": get_view_logs(doc.doctype, doc.name), "energy_point_logs": get_point_logs(doc.doctype, doc.name), + "additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name), "milestones": get_milestones(doc.doctype, doc.name), "is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user), "tags": get_tags(doc.doctype, doc.name), @@ -277,3 +278,14 @@ def get_document_email(doctype, name): def get_automatic_email_link(): return frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id") + +def get_additional_timeline_content(doctype, docname): + contents = [] + hooks = frappe.get_hooks().get('additional_timeline_content', {}) + methods_for_all_doctype = hooks.get('*', []) + methods_for_current_doctype = hooks.get(doctype, []) + + for method in methods_for_all_doctype + methods_for_current_doctype: + contents.extend(frappe.get_attr(method)(doctype, docname) or []) + + return contents \ No newline at end of file diff --git a/frappe/desk/form/save.py b/frappe/desk/form/save.py index 694b44b907..cae1bf5c77 100644 --- a/frappe/desk/form/save.py +++ b/frappe/desk/form/save.py @@ -18,12 +18,7 @@ def savedocs(doc, action): if doc.docstatus==1: doc.submit() else: - try: - doc.save() - except frappe.NameError as e: - doctype, name, original_exception = e if isinstance(e, tuple) else (doc.doctype or "", doc.name or "", None) - frappe.msgprint(frappe._("{0} {1} already exists").format(doctype, name)) - raise + doc.save() # update recent documents run_onload(doc) diff --git a/frappe/desk/leaderboard.py b/frappe/desk/leaderboard.py index 1ebf32febe..e5654c853f 100644 --- a/frappe/desk/leaderboard.py +++ b/frappe/desk/leaderboard.py @@ -14,13 +14,16 @@ def get_leaderboards(): return leaderboards @frappe.whitelist() -def get_energy_point_leaderboard(from_date, company = None, field = None, limit = None): +def get_energy_point_leaderboard(date_range, company = None, field = None, limit = None): + filters = [ + ['type', '!=', 'Review'], + ] + if date_range: + date_range = frappe.parse_json(date_range) + filters.append(['creation', 'between', [date_range[0], date_range[1]]]) energy_point_users = frappe.db.get_all('Energy Point Log', fields = ['user as name', 'sum(points) as value'], - filters = [ - ['type', '!=', 'Review'], - ['creation', '>', from_date] - ], + filters = filters, group_by = 'user', order_by = 'value desc' ) diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js index 4472a2978a..189949ac68 100644 --- a/frappe/desk/page/leaderboard/leaderboard.js +++ b/frappe/desk/page/leaderboard/leaderboard.js @@ -49,7 +49,7 @@ class Leaderboard { this.timespans = [ "This Week", "This Month", "This Quarter", "This Year", "Last Week", "Last Month", "Last Quarter", "Last Year", - "All Time", "Select From Date" + "All Time", "Select Date Range" ]; // for saving current selected filters @@ -113,7 +113,7 @@ class Leaderboard { return {"label": __(d), value: d }; }) ); - this.create_from_date_field(); + this.create_date_range_field(); this.type_select = this.page.add_select(__("Field"), this.options.selected_filter.map(d => { @@ -123,12 +123,12 @@ class Leaderboard { this.timespan_select.on("change", (e) => { this.options.selected_timespan = e.currentTarget.value; - if (this.options.selected_timespan === 'Select From Date') { - this.from_date_field.show(); + if (this.options.selected_timespan === 'Select Date Range') { + this.date_range_field.show(); } else { - this.from_date_field.hide(); - this.make_request(); + this.date_range_field.hide(); } + this.make_request(); }); this.type_select.on("change", (e) => { @@ -137,21 +137,21 @@ class Leaderboard { }); } - create_from_date_field() { + create_date_range_field() { let timespan_field = $(this.parent).find(`.frappe-control[data-original-title='Timespan']`); - this.from_date_field = $(`
`).insertAfter(timespan_field).hide(); + this.date_range_field = $(`
`).insertAfter(timespan_field).hide(); let date_field = frappe.ui.form.make_control({ df: { - fieldtype: 'Date', - fieldname: 'selected_from_date', - placeholder: frappe.datetime.month_start(), - default: frappe.datetime.month_start(), + fieldtype: 'DateRange', + fieldname: 'selected_date_range', + placeholder: "Date Range", + default: [frappe.datetime.month_start(), frappe.datetime.now_date()], input_class: 'input-sm', reqd: 1, change: () => { - this.selected_from_date = date_field.get_value(); - if (this.selected_from_date) this.make_request(); + this.selected_date_range = date_field.get_value(); + if (this.selected_date_range) this.make_request(); } }, parent: $(this.parent).find('.from-date-field'), @@ -225,7 +225,7 @@ class Leaderboard { frappe.call( this.leaderboard_config[this.options.selected_doctype].method, { - 'from_date': this.get_from_date(), + 'date_range': this.get_date_range(), 'company': this.options.selected_company, 'field': this.options.selected_filter_item, 'limit': this.leaderboard_limit, @@ -375,23 +375,22 @@ class Leaderboard { `); } - get_from_date() { + get_date_range() { let timespan = this.options.selected_timespan.toLowerCase(); let current_date = frappe.datetime.now_date(); - let get_from_date = { - "this week": frappe.datetime.week_start(), - "this month": frappe.datetime.month_start(), - "this quarter": frappe.datetime.quarter_start(), - "this year": frappe.datetime.year_start(), - "last week": frappe.datetime.add_days(current_date, -7), - "last month": frappe.datetime.add_months(current_date, -1), - "last quarter": frappe.datetime.add_months(current_date, -3), - "last year": frappe.datetime.add_months(current_date, -12), - "all time": "", - "select from date": this.selected_from_date || frappe.datetime.month_start() + let date_range_map = { + "this week": [frappe.datetime.week_start(), current_date], + "this month": [frappe.datetime.month_start(), current_date], + "this quarter": [frappe.datetime.quarter_start(), current_date], + "this year": [frappe.datetime.year_start(), current_date], + "last week": [frappe.datetime.add_days(current_date, -7), current_date], + "last month": [frappe.datetime.add_months(current_date, -1), current_date], + "last quarter": [frappe.datetime.add_months(current_date, -3), current_date], + "last year": [frappe.datetime.add_months(current_date, -12), current_date], + "all time": null, + "select date range": this.selected_date_range || [frappe.datetime.month_start(), current_date] } - - return get_from_date[timespan]; + return date_range_map[timespan]; } } diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 0edfd57d4f..d0a32ef076 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -67,8 +67,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None) # Reordered columns columns = json.loads(report.custom_columns) - if report.report_type == 'Query Report': - result = reorder_data_for_custom_columns(columns, query_columns, result) + result = reorder_data_for_custom_columns(columns, query_columns, result, report.report_type) result = add_data_to_custom_columns(columns, result) @@ -216,15 +215,21 @@ def add_data_to_custom_columns(columns, result): return data -def reorder_data_for_custom_columns(custom_columns, columns, result): +def reorder_data_for_custom_columns(custom_columns, columns, result, report_type): + custom_column_labels = [col["label"] for col in custom_columns] + + if report_type == 'Query Report': + original_column_labels = [col.split(":")[0] for col in columns] + else: + original_column_labels = [col["label"] for col in columns] + reordered_result = [] - columns = [col.split(":")[0] for col in columns] for res in result: r = [] - for col in custom_columns: + for col_name in custom_column_labels: try: - idx = columns.index(col.get("label")) + idx = original_column_labels.index(col_name) r.append(res[idx]) except ValueError: pass diff --git a/frappe/desk/search.py b/frappe/desk/search.py index dd5c9b7ab7..b4b54b4b6e 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import frappe, json from frappe.utils import cstr, unique, cint from frappe.permissions import has_permission +from frappe.handler import is_whitelisted from frappe import _ from six import string_types import re @@ -74,8 +75,17 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, if query and query.split()[0].lower()!="select": # by method - frappe.response["values"] = frappe.call(query, doctype, txt, - searchfield, start, page_length, filters, as_dict=as_dict) + try: + is_whitelisted(frappe.get_attr(query)) + frappe.response["values"] = frappe.call(query, doctype, txt, + searchfield, start, page_length, filters, as_dict=as_dict) + except Exception as e: + if frappe.local.conf.developer_mode: + raise e + else: + frappe.respond_as_web_page(title='Invalid Method', html='Method not found', + indicator_color='red', http_status_code=404) + return elif not query and doctype in standard_queries: # from standard queries search_widget(doctype, txt, standard_queries[doctype][0], diff --git a/frappe/email/__init__.py b/frappe/email/__init__.py index d58b35040e..0b874a03bb 100644 --- a/frappe/email/__init__.py +++ b/frappe/email/__init__.py @@ -57,6 +57,7 @@ def relink(name, reference_doctype=None, reference_name=None): communication_type = "Communication" and name = %s""", (reference_doctype, reference_name, name)) +@frappe.whitelist() def get_communication_doctype(doctype, txt, searchfield, start, page_len, filters): user_perms = frappe.utils.user.UserPermissions(frappe.session.user) user_perms.build_permissions() diff --git a/frappe/email/doctype/email_account/email_account.js b/frappe/email/doctype/email_account/email_account.js index 71f9cccb0d..83896e0af7 100644 --- a/frappe/email/doctype/email_account/email_account.js +++ b/frappe/email/doctype/email_account/email_account.js @@ -95,6 +95,11 @@ frappe.ui.form.on("Email Account", { enable_incoming: function(frm) { frm.doc.no_remaining = null; //perform full sync //frm.set_df_property("append_to", "reqd", frm.doc.enable_incoming); + frm.trigger("warn_autoreply_on_incoming"); + }, + + enable_auto_reply: function(frm) { + frm.trigger("warn_autoreply_on_incoming"); }, notify_if_unreplied: function(frm) { @@ -184,7 +189,18 @@ frappe.ui.form.on("Email Account", { read as well as unread message from server. This may also cause the duplication\ of Communication (emails)."); frappe.confirm(msg, null, function() { - frm.set_value("email_sync_option", "ALL"); + frm.set_value("email_sync_option", "UNSEEN"); + }); + } + }, + + warn_autoreply_on_incoming: function(frm) { + if (frm.doc.enable_incoming && frm.doc.enable_auto_reply && frm.doc.__islocal) { + var msg = __("Enabling auto reply on an incoming email account will send automated replies \ + to all the synchronized emails. Do you wish to continue?"); + frappe.confirm(msg, null, function() { + frm.set_value("enable_auto_reply", 0); + frappe.show_alert({message: __("Disabled Auto Reply"), indicator: "blue"}); }); } } diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 2065f5558a..cf8c6e80c6 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -478,26 +478,38 @@ class EmailAccount(Document): if self.append_to and self.sender_field: if self.subject_field: - # try and match by subject and sender - # if sent by same sender with same subject, - # append it to old coversation - subject = frappe.as_unicode(strip(re.sub(r"(^\s*(fw|fwd|wg)[^:]*:|\s*(re|aw)[^:]*:\s*)*", - "", email.subject, 0, flags=re.IGNORECASE))) + if '#' in email.subject: + # try and match if ID is found + # document ID is appended to subject + # example "Re: Your email (#OPP-2020-2334343)" + parent_id = email.subject.rsplit('#', 1)[-1].strip(' ()') + if parent_id: + parent = frappe.db.get_all(self.append_to, filters = dict(name = parent_id), + fields = 'name') - parent = frappe.db.get_all(self.append_to, filters={ - self.sender_field: email.from_email, - self.subject_field: ("like", "%{0}%".format(subject)), - "creation": (">", (get_datetime() - relativedelta(days=60)).strftime(DATE_FORMAT)) - }, fields="name") + if not parent: + # try and match by subject and sender + # if sent by same sender with same subject, + # append it to old coversation + subject = frappe.as_unicode(strip(re.sub(r"(^\s*(fw|fwd|wg)[^:]*:|\s*(re|aw)[^:]*:\s*)*", + "", email.subject, 0, flags=re.IGNORECASE))) + + parent = frappe.db.get_all(self.append_to, filters={ + self.sender_field: email.from_email, + self.subject_field: ("like", "%{0}%".format(subject)), + "creation": (">", (get_datetime() - relativedelta(days=60)).strftime(DATE_FORMAT)) + }, fields = "name", limit = 1) - # match only subject field - # when the from_email is of a user in the system - # and subject is atleast 10 chars long if not parent and len(subject) > 10 and is_system_user(email.from_email): + # match only subject field + # when the from_email is of a user in the system + # and subject is atleast 10 chars long parent = frappe.db.get_all(self.append_to, filters={ self.subject_field: ("like", "%{0}%".format(subject)), "creation": (">", (get_datetime() - relativedelta(days=60)).strftime(DATE_FORMAT)) - }, fields="name") + }, fields = "name", limit = 1) + + if parent: parent = frappe._dict(doctype=self.append_to, name=parent[0].name) diff --git a/frappe/email/doctype/email_account/test_email_account.py b/frappe/email/doctype/email_account/test_email_account.py index 29b54d7f8b..f87ee32bb1 100644 --- a/frappe/email/doctype/email_account/test_email_account.py +++ b/frappe/email/doctype/email_account/test_email_account.py @@ -5,7 +5,10 @@ from __future__ import unicode_literals import frappe, os import unittest, email -test_records = frappe.get_test_records('Email Account') +from frappe.test_runner import make_test_records + +make_test_records("User") +make_test_records("Email Account") from frappe.core.doctype.communication.email import make from frappe.desk.form.load import get_attachments diff --git a/frappe/email/doctype/email_queue/email_queue.json b/frappe/email/doctype/email_queue/email_queue.json index ee8683af22..4529ea8211 100644 --- a/frappe/email/doctype/email_queue/email_queue.json +++ b/frappe/email/doctype/email_queue/email_queue.json @@ -1,640 +1,166 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, + "actions": [], "autoname": "hash", - "beta": 0, "creation": "2012-08-02 15:17:28", - "custom": 0, "description": "Email Queue records.", - "docstatus": 0, "doctype": "DocType", "document_type": "System", - "editable_grid": 0, "engine": "InnoDB", + "field_order": [ + "sender", + "recipients", + "show_as_cc", + "message", + "status", + "error", + "message_id", + "reference_doctype", + "reference_name", + "communication", + "send_after", + "priority", + "add_unsubscribe_link", + "unsubscribe_param", + "unsubscribe_method", + "expose_recipients", + "attachments", + "retry" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "sender", "fieldtype": "Data", - "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, + "ignore_xss_filter": 1, "label": "Sender", - "length": 0, - "no_copy": 0, - "options": "Email", - "permlevel": 0, - "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 + "options": "Email" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "recipients", "fieldtype": "Table", - "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": "Recipient", - "length": 0, - "no_copy": 0, - "options": "Email Queue Recipient", - "permlevel": 0, - "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 + "options": "Email Queue Recipient" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "show_as_cc", "fieldtype": "Small Text", - "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": "Show as cc", - "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 + "label": "Show as cc" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "message", "fieldtype": "Code", - "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": "Message", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 + "label": "Message" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Not Sent", "fieldname": "status", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 1, "label": "Status", - "length": 0, - "no_copy": 0, - "options": "\nNot Sent\nSending\nSent\nError\nExpired", - "permlevel": 0, - "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 + "options": "\nNot Sent\nSending\nSent\nError\nExpired" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "error", "fieldtype": "Code", - "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": "Error", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 + "label": "Error" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "message_id", "fieldtype": "Data", - "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": "Message ID", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "search_index": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "reference_doctype", "fieldtype": "Link", - "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": "Reference Document Type", - "length": 0, - "no_copy": 0, "options": "DocType", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "reference_name", "fieldtype": "Data", - "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": "Reference DocName", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "communication", "fieldtype": "Link", - "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": "Communication", - "length": 0, - "no_copy": 0, "options": "Communication", - "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": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "search_index": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "send_after", "fieldtype": "Datetime", - "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": "Send After", - "length": 0, "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "priority", "fieldtype": "Int", - "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": "Priority", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "add_unsubscribe_link", "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": "Add Unsubscribe Link", - "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 + "label": "Add Unsubscribe Link" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "unsubscribe_param", "fieldtype": "Data", - "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": "Unsubscribe Param", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "unsubscribe_method", "fieldtype": "Data", - "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": "Unsubscribe Method", - "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 + "label": "Unsubscribe Method" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "expose_recipients", "fieldtype": "Data", - "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": "Expose Recipients", - "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 + "label": "Expose Recipients" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "attachments", "fieldtype": "Code", - "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": "Attachments", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "0", "fieldname": "retry", "fieldtype": "Int", - "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": "Retry", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "read_only": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, "icon": "fa fa-envelope", "idx": 1, - "image_view": 0, "in_create": 1, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-09-05 14:22:27.664645", + "links": [], + "modified": "2020-07-17 15:58:15.369419", "modified_by": "Administrator", "module": "Email", "name": "Email Queue", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 0, "delete": 1, "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "role": "System Manager" } ], - "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 + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index a82b52a663..48688afdb6 100755 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -107,6 +107,9 @@ class Newsletter(WebsiteGenerator): if self.get("__islocal"): throw(_("Please save the Newsletter before sending")) + if not self.recipients: + frappe.throw(_("Newsletter should have at least one recipient")) + def get_context(self, context): newsletters = get_newsletter_list("Newsletter", None, None, 0) if newsletters: diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index 8340d81917..d545190c47 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -11,6 +11,7 @@ import email.utils from six import iteritems, text_type, string_types from email.mime.multipart import MIMEMultipart from email.header import Header +from email import policy def get_email(recipients, sender='', msg='', subject='[No Subject]', @@ -68,8 +69,8 @@ class EMail: self.subject = subject self.expose_recipients = expose_recipients - self.msg_root = MIMEMultipart('mixed') - self.msg_alternative = MIMEMultipart('alternative') + self.msg_root = MIMEMultipart('mixed', policy=policy.SMTPUTF8) + self.msg_alternative = MIMEMultipart('alternative', policy=policy.SMTPUTF8) self.msg_root.attach(self.msg_alternative) self.cc = cc or [] self.bcc = bcc or [] @@ -100,7 +101,7 @@ class EMail: Attach message in the text portion of multipart/alternative """ from email.mime.text import MIMEText - part = MIMEText(message, 'plain', 'utf-8') + part = MIMEText(message, 'plain', 'utf-8', policy=policy.SMTPUTF8) self.msg_alternative.attach(part) def set_part_html(self, message, inline_images): @@ -113,9 +114,9 @@ class EMail: message, _inline_images = replace_filename_with_cid(message) # prepare parts - msg_related = MIMEMultipart('related') + msg_related = MIMEMultipart('related', policy=policy.SMTPUTF8) - html_part = MIMEText(message, 'html', 'utf-8') + html_part = MIMEText(message, 'html', 'utf-8', policy=policy.SMTPUTF8) msg_related.attach(html_part) for image in _inline_images: @@ -124,7 +125,7 @@ class EMail: self.msg_alternative.attach(msg_related) else: - self.msg_alternative.attach(MIMEText(message, 'html', 'utf-8')) + self.msg_alternative.attach(MIMEText(message, 'html', 'utf-8', policy=policy.SMTPUTF8)) def set_html_as_text(self, html): """Set plain text from HTML""" @@ -135,7 +136,7 @@ class EMail: from email.mime.text import MIMEText maintype, subtype = mime_type.split('/') - part = MIMEText(message, _subtype = subtype) + part = MIMEText(message, _subtype = subtype, policy=policy.SMTPUTF8) if as_attachment: part.add_header('Content-Disposition', 'attachment', filename=filename) @@ -222,7 +223,8 @@ class EMail: # reset headers as values may be changed. for key, val in iteritems(headers): - self.set_header(key, val) + if val: + self.set_header(key, val) # call hook to enable apps to modify msg_root before sending for hook in frappe.get_hooks("make_email_body_message"): @@ -238,7 +240,7 @@ class EMail: """validate, build message and convert to string""" self.validate() self.make() - return self.msg_root.as_string() + return self.msg_root.as_string(policy=policy.SMTPUTF8) def get_formatted_html(subject, message, footer=None, print_html=None, email_account=None, header=None, unsubscribe_link=None, sender=None): diff --git a/frappe/email/queue.py b/frappe/email/queue.py index ce512de276..8bffc108b9 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -347,7 +347,7 @@ def flush(from_test=False): if not smtpserver: smtpserver = SMTPServer() smtpserver_dict[email.sender] = smtpserver - + if from_test: send_one(email.name, smtpserver, auto_commit) else: @@ -390,12 +390,12 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False): where name=%s for update''', email, as_dict=True) - + if len(email): email = email[0] else: return - + recipients_list = frappe.db.sql('''select name, recipient, status from `tabEmail Queue Recipient` where parent=%s''', email.name, as_dict=1) @@ -417,6 +417,8 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False): if email.communication: frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit) + email_sent_to_any_recipient = None + try: message = None diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py index 43c4bb8333..705a853bc6 100644 --- a/frappe/email/test_email_body.py +++ b/frappe/email/test_email_body.py @@ -39,7 +39,7 @@ This is the text version of this email subject='Test Subject', content=email_html, text_content=email_text - ).as_string() + ).as_string().replace("\r\n", "\n") def test_prepare_message_returns_already_encoded_string(self): @@ -153,7 +153,7 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> subject='Test Subject', content=email_html, header=['Email Title', 'green'] - ).as_string() + ).as_string().replace("\r\n", "\n") self.assertTrue('''${__('Click here')}`])); + } +}); diff --git a/frappe/integrations/doctype/paytm_settings/paytm_settings.json b/frappe/integrations/doctype/paytm_settings/paytm_settings.json new file mode 100644 index 0000000000..93fbd0df09 --- /dev/null +++ b/frappe/integrations/doctype/paytm_settings/paytm_settings.json @@ -0,0 +1,89 @@ +{ + "actions": [], + "creation": "2020-04-02 00:11:22.846697", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "merchant_id", + "merchant_key", + "staging", + "column_break_4", + "industry_type_id", + "website" + ], + "fields": [ + { + "fieldname": "merchant_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Merchant ID", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "merchant_key", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Merchant Key", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0", + "fieldname": "staging", + "fieldtype": "Check", + "label": "Staging", + "show_days": 1, + "show_seconds": 1 + }, + { + "depends_on": "eval: !doc.staging", + "fieldname": "website", + "fieldtype": "Data", + "label": "Website", + "mandatory_depends_on": "eval: !doc.staging", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "depends_on": "eval: !doc.staging", + "fieldname": "industry_type_id", + "fieldtype": "Data", + "label": "Industry Type ID", + "mandatory_depends_on": "eval: !doc.staging", + "show_days": 1, + "show_seconds": 1 + } + ], + "issingle": 1, + "links": [], + "modified": "2020-06-08 13:36:09.703143", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Paytm Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/paytm_settings/paytm_settings.py b/frappe/integrations/doctype/paytm_settings/paytm_settings.py new file mode 100644 index 0000000000..616c3837d4 --- /dev/null +++ b/frappe/integrations/doctype/paytm_settings/paytm_settings.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import json +import requests +from six.moves.urllib.parse import urlencode + +import frappe +from frappe.model.document import Document +from frappe import _ +from frappe.utils import get_url, call_hook_method, cint, flt, cstr +from frappe.integrations.utils import create_request_log, create_payment_gateway +from frappe.utils import get_request_site_address +from paytmchecksum import generateSignature, verifySignature +from frappe.utils.password import get_decrypted_password + +class PaytmSettings(Document): + supported_currencies = ["INR"] + + def validate(self): + create_payment_gateway('Paytm') + call_hook_method('payment_gateway_enabled', gateway='Paytm') + + def validate_transaction_currency(self, currency): + if currency not in self.supported_currencies: + frappe.throw(_("Please select another payment method. Paytm does not support transactions in currency '{0}'").format(currency)) + + def get_payment_url(self, **kwargs): + '''Return payment url with several params''' + # create unique order id by making it equal to the integration request + integration_request = create_request_log(kwargs, "Host", "Paytm") + kwargs.update(dict(order_id=integration_request.name)) + + return get_url("./integrations/paytm_checkout?{0}".format(urlencode(kwargs))) + +def get_paytm_config(): + ''' Returns paytm config ''' + + paytm_config = frappe.db.get_singles_dict('Paytm Settings') + paytm_config.update(dict(merchant_key=get_decrypted_password('Paytm Settings', 'Paytm Settings', 'merchant_key'))) + + if cint(paytm_config.staging): + paytm_config.update(dict( + website="WEBSTAGING", + url='https://securegw-stage.paytm.in/order/process', + transaction_status_url='https://securegw-stage.paytm.in/order/status', + industry_type_id='RETAIL' + )) + else: + paytm_config.update(dict( + url='https://securegw.paytm.in/order/process', + transaction_status_url='https://securegw.paytm.in/order/status', + )) + return paytm_config + +def get_paytm_params(payment_details, order_id, paytm_config): + + # initialize a dictionary + paytm_params = dict() + + redirect_uri = get_request_site_address(True) + "/api/method/frappe.integrations.doctype.paytm_settings.paytm_settings.verify_transaction" + + + paytm_params.update({ + "MID" : paytm_config.merchant_id, + "WEBSITE" : paytm_config.website, + "INDUSTRY_TYPE_ID" : paytm_config.industry_type_id, + "CHANNEL_ID" : "WEB", + "ORDER_ID" : order_id, + "CUST_ID" : payment_details['payer_email'], + "EMAIL" : payment_details['payer_email'], + "TXN_AMOUNT" : cstr(flt(payment_details['amount'], 2)), + "CALLBACK_URL" : redirect_uri, + }) + + checksum = generateSignature(paytm_params, paytm_config.merchant_key) + + paytm_params.update({ + "CHECKSUMHASH" : checksum + }) + + return paytm_params + +@frappe.whitelist(allow_guest=True) +def verify_transaction(**paytm_params): + '''Verify checksum for received data in the callback and then verify the transaction''' + paytm_config = get_paytm_config() + is_valid_checksum = False + + paytm_params.pop('cmd', None) + paytm_checksum = paytm_params.pop('CHECKSUMHASH', None) + + if paytm_params and paytm_config and paytm_checksum: + # Verify checksum + is_valid_checksum = verifySignature(paytm_params, paytm_config.merchant_key, paytm_checksum) + + if is_valid_checksum and paytm_params.get('RESPCODE') == '01': + verify_transaction_status(paytm_config, paytm_params['ORDERID']) + else: + frappe.respond_as_web_page("Payment Failed", + "Transaction failed to complete. In case of any deductions, deducted amount will get refunded to your account.", + http_status_code=401, indicator_color='red') + frappe.log_error("Order unsuccessful. Failed Response:"+cstr(paytm_params), 'Paytm Payment Failed') + +def verify_transaction_status(paytm_config, order_id): + '''Verify transaction completion after checksum has been verified''' + paytm_params=dict( + MID=paytm_config.merchant_id, + ORDERID= order_id + ) + + checksum = generateSignature(paytm_params, paytm_config.merchant_key) + paytm_params["CHECKSUMHASH"] = checksum + + post_data = json.dumps(paytm_params) + url = paytm_config.transaction_status_url + + response = requests.post(url, data = post_data, headers = {"Content-type": "application/json"}).json() + finalize_request(order_id, response) + +def finalize_request(order_id, transaction_response): + request = frappe.get_doc('Integration Request', order_id) + transaction_data = frappe._dict(json.loads(request.data)) + redirect_to = transaction_data.get('redirect_to') or None + redirect_message = transaction_data.get('redirect_message') or None + + if transaction_response['STATUS'] == "TXN_SUCCESS": + if transaction_data.reference_doctype and transaction_data.reference_docname: + custom_redirect_to = None + try: + custom_redirect_to = frappe.get_doc(transaction_data.reference_doctype, + transaction_data.reference_docname).run_method("on_payment_authorized", 'Completed') + request.db_set('status', 'Completed') + except Exception: + request.db_set('status', 'Failed') + frappe.log_error(frappe.get_traceback()) + + if custom_redirect_to: + redirect_to = custom_redirect_to + + redirect_url = '/integrations/payment-success' + else: + request.db_set('status', 'Failed') + redirect_url = '/integrations/payment-failed' + + if redirect_to: + redirect_url += '?' + urlencode({'redirect_to': redirect_to}) + if redirect_message: + redirect_url += '&' + urlencode({'redirect_message': redirect_message}) + + frappe.local.response['type'] = 'redirect' + frappe.local.response['location'] = redirect_url + +def get_gateway_controller(doctype, docname): + reference_doc = frappe.get_doc(doctype, docname) + gateway_controller = frappe.db.get_value("Payment Gateway", reference_doc.payment_gateway, "gateway_controller") + return gateway_controller \ No newline at end of file diff --git a/frappe/core/doctype/video/test_video.py b/frappe/integrations/doctype/paytm_settings/test_paytm_settings.py similarity index 80% rename from frappe/core/doctype/video/test_video.py rename to frappe/integrations/doctype/paytm_settings/test_paytm_settings.py index 0bed1e98d6..77a16c82ae 100644 --- a/frappe/core/doctype/video/test_video.py +++ b/frappe/integrations/doctype/paytm_settings/test_paytm_settings.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe import unittest -class TestVideo(unittest.TestCase): +class TestPaytmSettings(unittest.TestCase): pass diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json index 830afbae53..123bb21e88 100755 --- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json +++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json @@ -74,11 +74,11 @@ }, { "default": "us-east-1", - "description": "See https://docs.aws.amazon.com/de_de/general/latest/gr/rande.html#s3_region for details.", + "description": "See https://docs.aws.amazon.com/general/latest/gr/s3.html for details.", "fieldname": "region", "fieldtype": "Select", "label": "Region", - "options": "us-east-1\nus-east-2\nus-west-1\nus-west-2\nap-south-1\nap-southeast-1\nap-southeast-2\nap-northeast-1\nap-northeast-2\nap-northeast-3\nca-central-1\ncn-north-1\ncn-northwest-1\neu-central-1\neu-west-1\neu-west-2\neu-west-3\neu-north-1\nsa-east-1" + "options": "us-east-1\nus-east-2\nus-west-1\nus-west-2\naf-south-1\nap-east-1\nap-south-1\nap-southeast-1\nap-southeast-2\nap-northeast-1\nap-northeast-2\nap-northeast-3\nca-central-1\ncn-north-1\ncn-northwest-1\neu-central-1\neu-west-1\neu-west-2\neu-west-3\neu-south-1\neu-north-1\nme-south-1\nsa-east-1" }, { "fieldname": "endpoint_url", @@ -151,7 +151,7 @@ "hide_toolbar": 1, "issingle": 1, "links": [], - "modified": "2020-04-13 20:57:24.432183", + "modified": "2020-07-27 17:27:21.400000", "modified_by": "Administrator", "module": "Integrations", "name": "S3 Backup Settings", @@ -172,4 +172,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py index 6cfd3646b2..c8b007ba7b 100755 --- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py +++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py @@ -19,6 +19,9 @@ from botocore.exceptions import ClientError class S3BackupSettings(Document): def validate(self): + if not self.enabled: + return + if not self.endpoint_url: self.endpoint_url = 'https://s3.amazonaws.com' conn = boto3.client( diff --git a/frappe/integrations/frappe_providers/__init__.py b/frappe/integrations/frappe_providers/__init__.py index 887e191e16..161937a936 100644 --- a/frappe/integrations/frappe_providers/__init__.py +++ b/frappe/integrations/frappe_providers/__init__.py @@ -7,7 +7,7 @@ from frappe.integrations.frappe_providers.frappecloud import frappecloud_migrato def migrate_to(local_site, frappe_provider): if frappe_provider in ("frappe.cloud", "frappecloud.com"): - return frappecloud_migrator(local_site, frappe_provider) + return frappecloud_migrator(local_site) else: print("{} is not supported yet".format(frappe_provider)) sys.exit(1) diff --git a/frappe/integrations/frappe_providers/frappecloud.py b/frappe/integrations/frappe_providers/frappecloud.py index 16bc09d9bf..e09f09a44b 100644 --- a/frappe/integrations/frappe_providers/frappecloud.py +++ b/frappe/integrations/frappe_providers/frappecloud.py @@ -1,412 +1,29 @@ -# imports - standard imports -import getpass -import json -import os -import re -import sys - -# imports - third party imports import click -from html2text import html2text import requests -from tenacity import retry, stop_after_attempt, wait_fixed +from html2text import html2text -# imports - module imports import frappe -import frappe.utils.backups -from frappe.utils import get_installed_apps_info -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 - - -@retry(stop=stop_after_attempt(5)) -def get_new_site_options(): - site_options_sc = session.post(options_url) - - if site_options_sc.ok: - site_options = site_options_sc.json()["message"] - return site_options - else: - print("Couldn't retrive New site information: {}".format(site_options_sc.status_code)) - - -def is_valid_subdomain(subdomain): - if len(subdomain) < 5: - print("Subdomain too short. Use 5 or more characters") - return False - matched = re.match("^[a-z0-9][a-z0-9-]*[a-z0-9]$", subdomain) - if matched: - return True - print("Subdomain contains invalid characters. Use lowercase characters, numbers and hyphens") - - -@retry(stop=stop_after_attempt(5)) -def is_subdomain_available(subdomain): - res = session.post(site_exists_url, {"subdomain": subdomain}) - if res.ok: - available = not res.json()["message"] - if not available: - print("Subdomain already exists! Try another one") - - return available - - -@add_line_after -def choose_plan(plans_list): - print("{} plans available".format(len(plans_list))) - available_plans = [plan["name"] for plan in plans_list] - render_plan_table(plans_list) - - while True: - input_plan = click.prompt("Select Plan").strip() - if input_plan in available_plans: - print("{} Plan selected ✅".format(input_plan)) - return input_plan - else: - print("Invalid Selection ❌") - - -@add_line_after -def check_app_compat(available_group): - is_compat = True - incompatible_apps, filtered_apps, branch_msgs = [], [], [] - existing_group = [(app["app_name"], app["branch"]) for app in get_installed_apps_info()] - print("Checking availability of existing app group") - - for (app, branch) in existing_group: - info = [ (a["name"], a["branch"]) for a in available_group["apps"] if a["scrubbed"] == app ] - if info: - app_title, available_branch = info[0] - - if branch != available_branch: - print("⚠️ App {}:{} => {}".format(app, branch, available_branch)) - branch_msgs.append([app, branch, available_branch]) - filtered_apps.append(app_title) - is_compat = False - - else: - print("✅ App {}:{}".format(app, branch)) - filtered_apps.append(app_title) - - else: - incompatible_apps.append(app) - print("❌ App {}:{}".format(app, branch)) - is_compat = False - - start_msg = "\nSelecting this group will " - incompatible_apps = ("\n\nDrop the following apps:\n" + "\n".join(incompatible_apps)) if incompatible_apps else "" - branch_change = ("\n\nUpgrade the following apps:\n" + "\n".join(["{}: {} => {}".format(*x) for x in branch_msgs])) if branch_msgs else "" - changes = (incompatible_apps + branch_change) or "be perfect for you :)" - warning_message = start_msg + changes - print(warning_message) - - return is_compat, filtered_apps - - -@add_line_after -def filter_apps(app_groups): - render_group_table(app_groups) - - while True: - app_group_index = click.prompt("Select App Group Number", type=int) - 1 - try: - if app_group_index == -1: - raise IndexError - selected_group = app_groups[app_group_index] - except IndexError: - print("Invalid Selection ❌") - continue - - is_compat, filtered_apps = check_app_compat(selected_group) - - if is_compat or click.confirm("Continue anyway?"): - print("App Group {} selected! ✅".format(selected_group["name"])) - break - - return selected_group["name"], filtered_apps - - -@add_line_after -def get_subdomain(domain): - while True: - subdomain = click.prompt("Enter subdomain").strip() - if is_valid_subdomain(subdomain) and is_subdomain_available(subdomain): - print("Site Domain: {}.{}".format(subdomain, domain)) - return subdomain - - -@retry(stop=stop_after_attempt(2), wait=wait_fixed(5)) -def upload_backup_file(file_type, file_path): - return session.post(files_url, data={}, files={ - "file": open(file_path, "rb"), - "is_private": 1, - "folder": "Home", - "method": "press.api.site.upload_backup", - "type": file_type - }) - - -@add_line_after -def upload_backup(local_site): - # take backup - files_session = {} - print("Taking backup for site {}".format(local_site)) - odb = frappe.utils.backups.new_backup(ignore_files=False, force=True) - - # upload files - for x, (file_type, file_path) in enumerate([ - ("database", odb.backup_path_db), - ("public", odb.backup_path_files), - ("private", odb.backup_path_private_files) - ]): - file_name = file_path.split(os.sep)[-1] - - print("Uploading {} file: {} ({}/3)".format(file_type, file_name, x+1)) - file_upload_response = upload_backup_file(file_type, file_path) - - if file_upload_response.ok: - files_session[file_type] = file_upload_response.json()["message"] - else: - print("Upload failed for: {}".format(file_path)) - - files_uploaded = { k: v["file_url"] for k, v in files_session.items() } - print("Uploaded backup files! ✅") - - return files_uploaded - - -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, - "Connection": "keep-alive" - }) - 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 - +def frappecloud_migrator(local_site): + print("Retreiving Site Migrator...") remote_site = frappe.conf.frappecloud_url or "frappecloud.com" + request_url = "https://{}/api/method/press.api.script".format(remote_site) + request = requests.get(request_url) - 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) + if request.status_code / 100 != 2: + print("Request exitted with Status Code: {}\nPayload: {}".format(request.status_code, html2text(request.text))) + click.secho("Some errors occurred while recovering the migration script. Please contact us @ Frappe Cloud if this issue persists", fg="yellow") + return - migrator_actions = [ - { "title": "Create a new site", "fn": new_site }, - { "title": "Restore to an existing site", "fn": restore_site } - ] + script_contents = request.json()["message"] - # get credentials + auth user + start session - session = create_session() + import tempfile + import os + import sys - # available actions defined in migrator_actions - primary_action = select_primary_action() - - primary_action(local_site) + py = sys.executable + script = tempfile.NamedTemporaryFile(mode="w") + script.write(script_contents) + print("Site Migrator stored at {}".format(script.name)) + os.execv(py, [py, script.name, local_site]) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index d7028870f4..09d303bec7 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -334,7 +334,7 @@ class BaseDocument(object): self.db_insert() return - frappe.msgprint(_("Duplicate name {0} {1}").format(self.doctype, self.name)) + frappe.msgprint(_("{0} {1} already exists").format(self.doctype, frappe.bold(self.name)), title=_("Duplicate Name"), indicator="red") raise frappe.DuplicateEntryError(self.doctype, self.name, e) elif frappe.db.is_unique_key_violation(e): @@ -702,16 +702,13 @@ class BaseDocument(object): df = self.meta.get_field(fieldname) sanitized_value = value - if df and df.get("fieldtype") in ("Data", "Code", "Small Text", "Text") and df.get("options")=="Email": - sanitized_value = sanitize_email(value) + if df and (df.get("ignore_xss_filter") + or (df.get("fieldtype") in ("Data", "Small Text", "Text") and df.get("options")=="Email") + or df.get("fieldtype") in ("Attach", "Attach Image", "Barcode", "Code") - elif df and (df.get("ignore_xss_filter") - or (df.get("fieldtype")=="Code" and df.get("options")!="Email") - or df.get("fieldtype") in ("Attach", "Attach Image", "Barcode") - - # cancelled and submit but not update after submit should be ignored - or self.docstatus==2 - or (self.docstatus==1 and not df.get("allow_on_submit"))): + # cancelled and submit but not update after submit should be ignored + or self.docstatus==2 + or (self.docstatus==1 and not df.get("allow_on_submit"))): continue else: diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py index 2142d544fe..fcf648e718 100644 --- a/frappe/model/create_new.py +++ b/frappe/model/create_new.py @@ -45,7 +45,9 @@ def make_new_doc(doctype): doc = doc.get_valid_dict(sanitize=False) doc["doctype"] = doctype doc["__islocal"] = 1 - doc["__unsaved"] = 1 + + if not frappe.model.meta.is_single(doctype): + doc["__unsaved"] = 1 return doc diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 19517aa4a1..ac87b1d907 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -203,7 +203,7 @@ class DatabaseQuery(object): def sanitize_fields(self): ''' regex : ^.*[,();].* - purpose : The regex will look for malicious patterns like `,`, '(', ')', ';' in each + purpose : The regex will look for malicious patterns like `,`, '(', ')', '@', ;' in each field which may leads to sql injection. example : field = "`DocType`.`issingle`, version()" @@ -211,11 +211,11 @@ class DatabaseQuery(object): the system will filter out this field. ''' - sub_query_regex = re.compile("^.*[,();].*") - blacklisted_keywords = ['select', 'create', 'insert', 'delete', 'drop', 'update', 'case'] + sub_query_regex = re.compile("^.*[,();@].*") + blacklisted_keywords = ['select', 'create', 'insert', 'delete', 'drop', 'update', 'case', 'show'] blacklisted_functions = ['concat', 'concat_ws', 'if', 'ifnull', 'nullif', 'coalesce', 'connection_id', 'current_user', 'database', 'last_insert_id', 'session_user', - 'system_user', 'user', 'version'] + 'system_user', 'user', 'version', 'global'] def _raise_exception(): frappe.throw(_('Use of sub-query or function is restricted'), frappe.DataError) @@ -238,6 +238,10 @@ class DatabaseQuery(object): if any("{0}(".format(keyword) in field.lower() for keyword in blacklisted_functions): _raise_exception() + if '@' in field.lower(): + # prevent access to global variables + _raise_exception() + if re.compile(r"[0-9a-zA-Z]+\s*'").match(field): _raise_exception() @@ -854,4 +858,4 @@ def get_date_range(operator, value): timespan = period_map[operator] + ' ' + timespan_map[value] if operator != 'timespan' else value - return get_timespan_date_range(timespan) \ No newline at end of file + return get_timespan_date_range(timespan) diff --git a/frappe/model/document.py b/frappe/model/document.py index 30d3442954..316c576f55 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -403,9 +403,16 @@ class Document(BaseDocument): def set_new_name(self, force=False, set_name=None, set_child_names=True): """Calls `frappe.naming.set_new_name` for parent and child docs.""" + if self.flags.name_set and not force: return + # If autoname has set as Prompt (name) + if self.get("__newname"): + self.name = self.get("__newname") + self.flags.name_set = True + return + if set_name: self.name = set_name else: @@ -830,7 +837,7 @@ class Document(BaseDocument): def run_notifications(self, method): """Run notifications for this method""" - if frappe.flags.in_import or frappe.flags.in_patch or frappe.flags.in_install: + if (frappe.flags.in_import and frappe.flags.mute_emails) or frappe.flags.in_patch or frappe.flags.in_install: return if self.flags.notifications_executed==None: @@ -1300,6 +1307,16 @@ class Document(BaseDocument): users = set([assignment.owner for assignment in assignments]) return users + def add_tag(self, tag): + """Add a Tag to this document""" + from frappe.desk.doctype.tag.tag import DocTags + DocTags(self.doctype).add(self.name, tag) + + def get_tags(self): + """Return a list of Tags attached to this document""" + from frappe.desk.doctype.tag.tag import DocTags + return DocTags(self.doctype).get_tags(self.name).split(",")[1:] + def execute_action(doctype, name, action, **kwargs): """Execute an action on a document (called by background worker)""" doc = frappe.get_doc(doctype, name) diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py index ea563dfc13..32919b3333 100644 --- a/frappe/model/workflow.py +++ b/frappe/model/workflow.py @@ -307,4 +307,4 @@ def set_workflow_state_on_action(doc, workflow_name, action): for state in workflow.states: if state.doc_status == docstatus: doc.set(workflow_state_field, state.state) - return \ No newline at end of file + return diff --git a/frappe/modules/export_file.py b/frappe/modules/export_file.py index b904132530..4b22c82105 100644 --- a/frappe/modules/export_file.py +++ b/frappe/modules/export_file.py @@ -12,16 +12,17 @@ def export_doc(doc): def export_to_files(record_list=None, record_module=None, verbose=0, create_init=None): """ - Export record_list to files. record_list is a list of lists ([doctype],[docname] ) , + Export record_list to files. record_list is a list of lists ([doctype, docname, folder name],) , """ if frappe.flags.in_import: return if record_list: for record in record_list: - write_document_file(frappe.get_doc(record[0], record[1]), record_module, create_init=create_init) + folder_name = record[2] if len(record) == 3 else None + write_document_file(frappe.get_doc(record[0], record[1]), record_module, create_init=create_init, folder_name=folder_name) -def write_document_file(doc, record_module=None, create_init=True): +def write_document_file(doc, record_module=None, create_init=True, folder_name=None): newdoc = doc.as_dict(no_nulls=True) doc.run_method("before_export", newdoc) @@ -35,7 +36,10 @@ def write_document_file(doc, record_module=None, create_init=True): module = record_module or get_module_name(doc) # create folder - folder = create_folder(module, doc.doctype, doc.name, create_init) + if folder_name: + folder = create_folder(module, folder_name, doc.name, create_init) + else: + folder = create_folder(module, doc.doctype, doc.name, create_init) # write the data file fname = scrub(doc.name) diff --git a/frappe/oauth.py b/frappe/oauth.py index 4dc50366be..122c806072 100644 --- a/frappe/oauth.py +++ b/frappe/oauth.py @@ -4,6 +4,7 @@ import pytz from frappe import _ from frappe.auth import LoginManager +from http import cookies from oauthlib.oauth2.rfc6749.tokens import BearerToken from oauthlib.oauth2.rfc6749.grant_types import AuthorizationCodeGrant, ImplicitGrant, ResourceOwnerPasswordCredentialsGrant, ClientCredentialsGrant, RefreshTokenGrant from oauthlib.oauth2 import RequestValidator @@ -130,15 +131,12 @@ class OAuthWebRequestValidator(RequestValidator): oac.scopes = get_url_delimiter().join(request.scopes) oac.redirect_uri_bound_to_authorization_code = request.redirect_uri oac.client = client_id - oac.user = unquote(cookie_dict['user_id']) + oac.user = unquote(cookie_dict['user_id'].value) oac.authorization_code = code['code'] oac.save(ignore_permissions=True) frappe.db.commit() def authenticate_client(self, request, *args, **kwargs): - - cookie_dict = get_cookie_dict_from_headers(request) - #Get ClientID in URL if request.client_id: oc = frappe.get_doc("OAuth Client", request.client_id) @@ -155,7 +153,9 @@ class OAuthWebRequestValidator(RequestValidator): except Exception as e: print("Failed body authentication: Application %s does not exist".format(cid=request.client_id)) - return frappe.session.user == unquote(cookie_dict.get('user_id', "Guest")) + cookie_dict = get_cookie_dict_from_headers(request) + user_id = unquote(cookie_dict['user_id']) if 'user_id' in cookie_dict else "Guest" + return frappe.session.user == user_id def authenticate_client_id(self, client_id, request, *args, **kwargs): cli_id = frappe.db.get_value('OAuth Client', client_id, 'name') @@ -400,13 +400,10 @@ class OAuthWebRequestValidator(RequestValidator): return True def get_cookie_dict_from_headers(r): + cookie = cookies.BaseCookie() if r.headers.get('Cookie'): - cookie = r.headers.get('Cookie') - cookie = cookie.split("; ") - cookie_dict = {k:v for k,v in (x.split('=') for x in cookie)} - return cookie_dict - else: - return {} + cookie.load(r.headers.get('Cookie')) + return cookie def calculate_at_hash(access_token, hash_alg): """Helper method for calculating an access token diff --git a/frappe/patches.txt b/frappe/patches.txt index a03d31918b..75750ab59c 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -19,6 +19,7 @@ execute:frappe.reload_doc('core', 'doctype', 'module_def') #2017-09-22 execute:frappe.reload_doc('core', 'doctype', 'version') #2017-04-01 execute:frappe.reload_doc('email', 'doctype', 'document_follow') execute:frappe.reload_doc('core', 'doctype', 'communication_link') #2019-10-02 +execute:frappe.reload_doc('core', 'doctype', 'has_role') execute:frappe.reload_doc('core', 'doctype', 'communication') #2019-10-02 frappe.patches.v11_0.replicate_old_user_permissions frappe.patches.v11_0.reload_and_rename_view_log #2019-01-03 @@ -263,6 +264,7 @@ frappe.patches.v11_0.make_all_prepared_report_attachments_private #2019-11-26 frappe.patches.v12_0.setup_email_linking frappe.patches.v12_0.fix_home_settings_for_all_users frappe.patches.v12_0.change_existing_dashboard_chart_filters +frappe.patches.v12_0.set_correct_assign_value_in_docs #2020-07-13 execute:frappe.delete_doc("Test Runner") execute:frappe.delete_doc_if_exists('DocType', 'Google Maps Settings') execute:frappe.db.set_default('desktop:home_page', 'workspace') @@ -271,7 +273,9 @@ execute:frappe.delete_doc_if_exists('DocType', 'GSuite Templates') 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 +frappe.patches.v12_0.remove_example_email_thread_notify execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders() +frappe.patches.v12_0.set_correct_url_in_files frappe.patches.v13_0.website_theme_custom_scss frappe.patches.v13_0.set_existing_dashboard_charts_as_public frappe.patches.v13_0.set_path_for_homepage_in_web_page_view @@ -289,4 +293,7 @@ execute:frappe.delete_doc("DocType", "Onboarding Slide Field") execute:frappe.delete_doc("DocType", "Onboarding Slide Help Link") frappe.patches.v13_0.update_date_filters_in_user_settings frappe.patches.v13_0.update_duration_options -frappe.patches.v13_0.replace_old_data_import +frappe.patches.v13_0.replace_old_data_import # 2020-06-24 +frappe.patches.v13_0.create_custom_dashboards_cards_and_charts +frappe.patches.v13_0.rename_is_custom_field_in_dashboard_chart +frappe.patches.v13_0.generate_theme_files_in_public_folder diff --git a/frappe/patches/v11_0/reload_and_rename_view_log.py b/frappe/patches/v11_0/reload_and_rename_view_log.py index 611de79a3c..12c71b746f 100644 --- a/frappe/patches/v11_0/reload_and_rename_view_log.py +++ b/frappe/patches/v11_0/reload_and_rename_view_log.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import frappe def execute(): - if frappe.db.exists('DocType', 'View log'): + if frappe.db.table_exists('View log'): # for mac users direct renaming would not work since mysql for mac saves table name in lower case # so while renaming `tabView log` to `tabView Log` we get "Table 'tabView Log' already exists" error # more info https://stackoverflow.com/a/44753093/5955589 , diff --git a/frappe/patches/v12_0/remove_example_email_thread_notify.py b/frappe/patches/v12_0/remove_example_email_thread_notify.py new file mode 100644 index 0000000000..94959b6077 --- /dev/null +++ b/frappe/patches/v12_0/remove_example_email_thread_notify.py @@ -0,0 +1,8 @@ +import frappe + + +def execute(): + # remove all example.com email user accounts from notifications + frappe.db.sql("""UPDATE `tabUser` + SET thread_notify=0, send_me_a_copy=0 + WHERE email like '%@example.com'""") diff --git a/frappe/patches/v12_0/set_correct_assign_value_in_docs.py b/frappe/patches/v12_0/set_correct_assign_value_in_docs.py new file mode 100644 index 0000000000..65a635c170 --- /dev/null +++ b/frappe/patches/v12_0/set_correct_assign_value_in_docs.py @@ -0,0 +1,32 @@ +import frappe + +def execute(): + frappe.reload_doc('desk', 'doctype', 'todo') + + query = ''' + SELECT + name, reference_type, reference_name, {} as assignees + FROM + `tabToDo` + WHERE + COALESCE(reference_type, '') != '' AND + COALESCE(reference_name, '') != '' AND + status != 'Cancelled' + GROUP BY + reference_type, reference_name + ''' + + assignments = frappe.db.multisql({ + 'mariadb': query.format('GROUP_CONCAT(DISTINCT `owner`)'), + 'postgres': query.format('STRING_AGG(DISTINCT "owner", ",")') + }, as_dict=True) + + for doc in assignments: + assignments = doc.assignees.split(',') + frappe.db.set_value( + doc.reference_type, + doc.reference_name, + '_assign', + frappe.as_json(assignments), + update_modified=False + ) diff --git a/frappe/patches/v12_0/set_correct_url_in_files.py b/frappe/patches/v12_0/set_correct_url_in_files.py new file mode 100644 index 0000000000..4f820c1b24 --- /dev/null +++ b/frappe/patches/v12_0/set_correct_url_in_files.py @@ -0,0 +1,39 @@ +import frappe +import os + +def execute(): + files = frappe.get_all('File', + fields = ['name', 'file_name', 'file_url'], + filters = { + 'is_folder': 0, + 'file_url': ['!=', ''], + }) + + private_file_path = frappe.get_site_path('private', 'files') + public_file_path = frappe.get_site_path('public', 'files') + + for file in files: + file_path = file.file_url + file_name = file_path.split('/')[-1] + + if not file_path.startswith(('/private/', '/files/')): + continue + + file_is_private = file_path.startswith('/private/files/') + full_path = frappe.utils.get_files_path(file_name, is_private=file_is_private) + + if not os.path.exists(full_path): + if file_is_private: + public_file_url = os.path.join(public_file_path, file_name) + if os.path.exists(public_file_url): + frappe.db.set_value('File', file.name, { + 'file_url': '/files/{0}'.format(file_name), + 'is_private': 0 + }) + else: + private_file_url = os.path.join(private_file_path, file_name) + if os.path.exists(private_file_url): + frappe.db.set_value('File', file.name, { + 'file_url': '/private/files/{0}'.format(file_name), + 'is_private': 1 + }) diff --git a/frappe/patches/v13_0/create_custom_dashboards_cards_and_charts.py b/frappe/patches/v13_0/create_custom_dashboards_cards_and_charts.py new file mode 100644 index 0000000000..9a075a22cc --- /dev/null +++ b/frappe/patches/v13_0/create_custom_dashboards_cards_and_charts.py @@ -0,0 +1,45 @@ +import frappe +from frappe.model.naming import append_number_if_name_exists +from frappe.utils.dashboard import get_dashboards_with_link + +def execute(): + if not frappe.db.table_exists('Dashboard Chart')\ + or not frappe.db.table_exists('Number Card')\ + or not frappe.db.table_exists('Dashboard'): + return + + frappe.reload_doc('desk', 'doctype', 'dashboard_chart') + frappe.reload_doc('desk', 'doctype', 'number_card') + frappe.reload_doc('desk', 'doctype', 'dashboard') + + modified_charts = get_modified_docs('Dashboard Chart') + modified_cards = get_modified_docs('Number Card') + modified_dashboards = [doc.name for doc in get_modified_docs('Dashboard')] + + for chart in modified_charts: + modified_dashboards += get_dashboards_with_link(chart.name, 'Dashboard Chart') + rename_modified_doc(chart.name, 'Dashboard Chart') + + for card in modified_cards: + modified_dashboards += get_dashboards_with_link(card.name, 'Number Card') + rename_modified_doc(card.name, 'Number Card') + + modified_dashboards = list(set(modified_dashboards)) + + for dashboard in modified_dashboards: + rename_modified_doc(dashboard, 'Dashboard') + +def get_modified_docs(doctype): + return frappe.get_all(doctype, + filters = { + 'owner': 'Administrator', + 'modified_by': ['!=', 'Administrator'] + }) + +def rename_modified_doc(docname, doctype): + new_name = docname + ' Custom' + try: + frappe.rename_doc(doctype, docname, new_name) + except frappe.ValidationError: + new_name = append_number_if_name_exists(doctype, new_name) + frappe.rename_doc(doctype, docname, new_name) diff --git a/frappe/patches/v13_0/generate_theme_files_in_public_folder.py b/frappe/patches/v13_0/generate_theme_files_in_public_folder.py new file mode 100644 index 0000000000..c5a64780cd --- /dev/null +++ b/frappe/patches/v13_0/generate_theme_files_in_public_folder.py @@ -0,0 +1,15 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe + + +def execute(): + themes = frappe.db.get_all( + "Website Theme", filters={"theme_url": ("not like", "/files/website_theme/%")} + ) + for theme in themes: + doc = frappe.get_doc("Website Theme", theme.name) + doc.generate_bootstrap_theme() + doc.save() diff --git a/frappe/patches/v13_0/rename_is_custom_field_in_dashboard_chart.py b/frappe/patches/v13_0/rename_is_custom_field_in_dashboard_chart.py new file mode 100644 index 0000000000..4da0f8164a --- /dev/null +++ b/frappe/patches/v13_0/rename_is_custom_field_in_dashboard_chart.py @@ -0,0 +1,11 @@ +import frappe +from frappe.model.utils.rename_field import rename_field + +def execute(): + if not frappe.db.table_exists('Dashboard Chart'): + return + + frappe.reload_doc('desk', 'doctype', 'dashboard_chart') + + if frappe.db.has_column('Dashboard Chart', 'is_custom'): + rename_field('Dashboard Chart', 'is_custom', 'use_report_chart') \ No newline at end of file diff --git a/frappe/patches/v13_0/replace_old_data_import.py b/frappe/patches/v13_0/replace_old_data_import.py index 1c00ae5f34..920ee7b553 100644 --- a/frappe/patches/v13_0/replace_old_data_import.py +++ b/frappe/patches/v13_0/replace_old_data_import.py @@ -6,9 +6,15 @@ import frappe def execute(): - frappe.rename_doc('DocType', 'Data Import', 'Data Import Legacy') + if not frappe.db.table_exists("Data Import"): return + + meta = frappe.get_meta("Data Import") + # if Data Import is the new one, return early + if meta.fields[1].fieldname == "import_type": + return + + frappe.db.sql("DROP TABLE IF EXISTS `tabData Import Legacy`") + frappe.rename_doc("DocType", "Data Import", "Data Import Legacy") frappe.db.commit() frappe.db.sql("DROP TABLE IF EXISTS `tabData Import`") - frappe.reload_doc("core", "doctype", "data_import") - frappe.get_doc("DocType", "Data Import").on_update() - frappe.delete_doc_if_exists("DocType", "Data Import Beta") + frappe.rename_doc("DocType", "Data Import Beta", "Data Import") diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js index 252c706e51..e6599b2496 100644 --- a/frappe/printing/doctype/print_format/print_format.js +++ b/frappe/printing/doctype/print_format/print_format.js @@ -35,13 +35,20 @@ frappe.ui.form.on("Print Format", { else if (frm.doc.custom_format && !frm.doc.raw_printing) { frm.set_df_property("html", "reqd", 1); } - frm.add_custom_button(__("Make Default"), function () { - frappe.call({ - method: "frappe.printing.doctype.print_format.print_format.make_default", - args: { - name: frm.doc.name - } - }) + frappe.db.get_value('DocType', frm.doc.doc_type, 'default_print_format', (r) => { + if (r.default_print_format != frm.doc.name) { + frm.add_custom_button(__("Set as Default"), function () { + frappe.call({ + method: "frappe.printing.doctype.print_format.print_format.make_default", + args: { + name: frm.doc.name + }, + callback: function() { + frm.refresh(); + } + }); + }); + } }); } }, diff --git a/frappe/printing/doctype/print_settings/print_settings.json b/frappe/printing/doctype/print_settings/print_settings.json index 397d9dda5d..f93ad0ee5a 100644 --- a/frappe/printing/doctype/print_settings/print_settings.json +++ b/frappe/printing/doctype/print_settings/print_settings.json @@ -1,932 +1,203 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, + "actions": [], "creation": "2014-07-17 06:54:20.782907", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "document_type": "System", - "editable_grid": 0, + "engine": "InnoDB", + "field_order": [ + "pdf_settings", + "send_print_as_pdf", + "repeat_header_footer", + "column_break_4", + "pdf_page_size", + "view_link_in_email", + "with_letterhead", + "allow_print_for_draft", + "add_draft_heading", + "column_break_10", + "allow_page_break_inside_tables", + "allow_print_for_cancelled", + "server_printer", + "enable_print_server", + "server_ip", + "printer_name", + "port", + "raw_printing_section", + "enable_raw_printing", + "print_style_section", + "print_style", + "print_style_preview", + "section_break_8", + "font", + "font_size" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "pdf_settings", "fieldtype": "Section Break", - "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": "PDF Settings", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 + "label": "PDF Settings" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "description": "Send Email Print Attachments as PDF (Recommended)", - "fetch_if_empty": 0, "fieldname": "send_print_as_pdf", "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": "Send Print as PDF", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 + "label": "Send Print as PDF" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", - "fetch_if_empty": 0, "fieldname": "repeat_header_footer", "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": "Repeat Header and Footer in PDF", - "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 + "label": "Repeat Header and Footer in PDF" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "column_break_4", - "fieldtype": "Column Break", - "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, - "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 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "A4", - "fetch_if_empty": 0, "fieldname": "pdf_page_size", "fieldtype": "Select", - "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": "PDF Page Size", - "length": 0, - "no_copy": 0, - "options": "A4\nLetter", - "permlevel": 0, - "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 + "options": "A4\nLetter" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "view_link_in_email", "fieldtype": "Section Break", - "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": "Page Settings", - "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 + "label": "Page Settings" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", - "description": "", - "fetch_if_empty": 0, "fieldname": "with_letterhead", "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": "Print with letterhead", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 + "label": "Print with letterhead" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", - "description": "", - "fetch_if_empty": 0, "fieldname": "allow_print_for_draft", "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": "Allow Print for Draft", - "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 + "label": "Allow Print for Draft" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "1", - "description": "", - "fetch_if_empty": 0, - "fieldname": "attach_view_link", - "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": "Send document web view link in email", - "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, - "fetch_if_empty": 0, "fieldname": "column_break_10", - "fieldtype": "Column Break", - "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, - "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 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", - "fetch_if_empty": 0, "fieldname": "add_draft_heading", "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": "Always add \"Draft\" Heading for printing draft documents", - "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 + "label": "Always add \"Draft\" Heading for printing draft documents" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "allow_page_break_inside_tables", "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": "Allow page break inside tables", - "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 + "label": "Allow page break inside tables" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fetch_if_empty": 0, + "default": "0", "fieldname": "allow_print_for_cancelled", "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": "Allow Print for Cancelled", - "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 + "label": "Allow Print for Cancelled" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, "fieldname": "server_printer", "fieldtype": "Section Break", - "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": "Print Server", - "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 + "label": "Print Server" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "enable_print_server", "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": "Enable Print Server", - "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 + "label": "Enable Print Server" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "localhost", "depends_on": "enable_print_server", - "fetch_if_empty": 0, "fieldname": "server_ip", "fieldtype": "Data", - "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": "Server IP", - "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 + "label": "Server IP" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "enable_print_server", - "fetch_if_empty": 0, "fieldname": "printer_name", "fieldtype": "Select", - "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": "Printer Name", - "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 + "label": "Printer Name" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "631", "depends_on": "enable_print_server", - "fetch_if_empty": 0, "fieldname": "port", "fieldtype": "Int", - "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": "Port", - "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 + "label": "Port" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "raw_printing_section", "fieldtype": "Section Break", - "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": "Raw Printing", - "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 + "label": "Raw Printing" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "enable_raw_printing", "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": "Enable Raw Printing", - "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 + "label": "Enable Raw Printing" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "print_style_section", "fieldtype": "Section Break", - "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": "Print Style", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 + "label": "Print Style" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Modern", - "fetch_if_empty": 0, "fieldname": "print_style", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Print Style", - "length": 0, - "no_copy": 0, - "options": "Print Style", - "permlevel": 0, - "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 + "options": "Print Style" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "print_style_preview", "fieldtype": "HTML", - "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": "Print Style Preview", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 + "label": "Print Style Preview" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "section_break_8", "fieldtype": "Section Break", - "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": "Fonts", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 + "label": "Fonts" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Default", - "fetch_if_empty": 0, "fieldname": "font", "fieldtype": "Select", - "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": "Font", - "length": 0, - "no_copy": 0, - "options": "Default\nArial\nHelvetica\nVerdana\nMonospace", - "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 + "options": "Default\nArial\nHelvetica\nVerdana\nMonospace" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "In points. Default is 9.", - "fetch_if_empty": 0, "fieldname": "font_size", "fieldtype": "Float", - "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": "Font Size", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 + "label": "Font Size" } ], - "has_web_view": 0, - "hide_toolbar": 0, "icon": "fa fa-cog", - "idx": 0, - "in_create": 0, - "is_submittable": 0, "issingle": 1, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2019-04-10 14:12:31.081187", + "links": [], + "modified": "2020-07-02 16:14:47.470668", "modified_by": "Administrator", "module": "Printing", "name": "Print Settings", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, "read": 1, - "report": 0, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], "quick_entry": 1, - "read_only": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/public/css/email.css b/frappe/public/css/email.css index 40c6149927..5c398009ff 100644 --- a/frappe/public/css/email.css +++ b/frappe/public/css/email.css @@ -7,6 +7,12 @@ body { p { margin: 1em 0 !important; } +.ql-editor { + white-space: normal; +} +.ql-editor p { + margin: 0 !important; +} hr { border-top: 1px solid #d1d8dd; } diff --git a/frappe/public/js/frappe/data_import/data_exporter.js b/frappe/public/js/frappe/data_import/data_exporter.js index 735237189d..f6af338235 100644 --- a/frappe/public/js/frappe/data_import/data_exporter.js +++ b/frappe/public/js/frappe/data_import/data_exporter.js @@ -13,36 +13,6 @@ frappe.data_import.DataExporter = class DataExporter { this.dialog = new frappe.ui.Dialog({ title: __('Export Data'), fields: [ - { - fieldtype: 'Select', - fieldname: 'exporting_for', - label: __('Exporting For'), - options: [ - { - label: __('Insert New Records'), - value: 'Insert New Records' - }, - { - label: __('Update Existing Records'), - value: 'Update Existing Records' - } - ], - change: () => { - let exporting_for = this.dialog.get_value('exporting_for'); - this.dialog.set_value( - 'export_records', - exporting_for === 'Insert New Records' ? 'blank_template' : 'all' - ); - - // Force ID field to be exported when updating existing records - let id_field = this.dialog.get_field(this.doctype).options[0]; - if (id_field.value === 'name' && id_field.$checkbox) { - id_field.$checkbox - .find('input') - .prop('disabled', exporting_for === 'Update Existing Records'); - } - } - }, { fieldtype: 'Select', fieldname: 'export_records', @@ -65,7 +35,7 @@ frappe.data_import.DataExporter = class DataExporter { value: 'blank_template' } ], - default: 'blank_template', + default: this.exporting_for === 'Insert New Records' ? 'blank_template' : 'all', change: () => { this.update_record_count_message(); } @@ -119,10 +89,6 @@ frappe.data_import.DataExporter = class DataExporter { on_page_show: () => this.select_mandatory() }); - if (this.exporting_for) { - this.dialog.set_value('exporting_for', this.exporting_for); - } - this.make_filter_area(); this.make_select_all_buttons(); this.update_record_count_message(); @@ -172,15 +138,17 @@ frappe.data_import.DataExporter = class DataExporter { } make_select_all_buttons() { + let for_insert = this.exporting_for === 'Insert New Records'; + let section_title = for_insert ? __('Select Fields To Insert') : __('Select Fields To Update'); let $select_all_buttons = $(`
-
${__('Select fields to export')}
+
${section_title}
- + `: ''} @@ -285,11 +253,9 @@ frappe.data_import.DataExporter = class DataExporter { } get_filters() { - return this.filter_group.get_filters().reduce((acc, filter) => { - return Object.assign(acc, { - [filter[1]]: [filter[2], filter[3]] - }); - }, {}); + return this.filter_group.get_filters().map(filter => { + return filter.slice(0, 4); + }); } get_multicheck_options(doctype, child_fieldname = null) { @@ -308,6 +274,9 @@ frappe.data_import.DataExporter = class DataExporter { ? this.column_map[child_fieldname] : this.column_map[doctype]; + let is_field_mandatory = df => (df.fieldname === 'name' && !child_fieldname) + || (df.reqd && this.exporting_for == 'Insert New Records'); + return fields .filter(df => { if (autoname_field && df.fieldname === autoname_field.fieldname) { @@ -323,7 +292,7 @@ frappe.data_import.DataExporter = class DataExporter { return { label, value: df.fieldname, - danger: df.reqd, + danger: is_field_mandatory(df), checked: false, description: `${df.fieldname} ${df.reqd ? __('(Mandatory)') : ''}` }; diff --git a/frappe/public/js/frappe/data_import/import_preview.js b/frappe/public/js/frappe/data_import/import_preview.js index 7cf8431456..4edcb87aeb 100644 --- a/frappe/public/js/frappe/data_import/import_preview.js +++ b/frappe/public/js/frappe/data_import/import_preview.js @@ -245,11 +245,12 @@ frappe.data_import.ImportPreview = class ImportPreview { let fieldname; if (!df) { fieldname = null; + } else if (col.map_to_field) { + fieldname = col.map_to_field; + } else if (col.is_child_table_field) { + fieldname = `${col.child_table_df.fieldname}.${df.fieldname}`; } else { - fieldname = - df.parent === this.doctype - ? df.fieldname - : `${df.parent}:${df.fieldname}`; + fieldname = df.fieldname; } return [ { @@ -272,7 +273,7 @@ frappe.data_import.ImportPreview = class ImportPreview { label: __("Don't Import"), value: "Don't Import" } - ].concat(column_picker_fields.get_fields_as_options()), + ].concat(get_fields_as_options(this.doctype, column_picker_fields)), default: fieldname || "Don't Import", change() { changed.push(i); @@ -328,3 +329,29 @@ frappe.data_import.ImportPreview = class ImportPreview { }); } }; + +function get_fields_as_options(doctype, column_map) { + let keys = [doctype]; + frappe.meta.get_table_fields(doctype).forEach(df => { + keys.push(df.fieldname); + }); + // flatten array + return [].concat( + ...keys.map(key => { + return column_map[key].map(df => { + let label = df.label; + let value = df.fieldname; + if (doctype !== key) { + let table_field = frappe.meta.get_docfield(doctype, key); + label = `${df.label} (${table_field.label})`; + value = `${table_field.fieldname}.${df.fieldname}`; + } + return { + label, + value, + description: value + }; + }); + }) + ); +} \ No newline at end of file diff --git a/frappe/public/js/frappe/db.js b/frappe/public/js/frappe/db.js index 1b6fb0e438..cf716c67e5 100644 --- a/frappe/public/js/frappe/db.js +++ b/frappe/public/js/frappe/db.js @@ -91,12 +91,26 @@ frappe.db = { }); }, count: function(doctype, args={}) { - return new Promise(resolve => { - frappe.call({ - method: 'frappe.client.get_count', - type: 'GET', - args: Object.assign(args, { doctype }) - }).then(r => resolve(r.message)); + let filters = args.filters || {}; + const with_child_table_filter = Array.isArray(filters) && filters.some(filter => { + return filter[0] !== doctype; + }); + + const fields = [ + // cannot break this line as it adds extra \n's and \t's which breaks the query + `count(${with_child_table_filter ? 'distinct': ''} ${frappe.model.get_full_column_name('name', doctype)}) AS total_count` + ]; + + return frappe.call({ + type: 'GET', + method: 'frappe.desk.reportview.get', + args: { + doctype, + filters, + fields, + } + }).then(r => { + return r.message.values[0][0]; }); }, get_link_options(doctype, txt = '', filters={}) { diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 79a78717cb..2e80dbfd85 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -101,15 +101,6 @@ frappe.Application = Class.extend({ frappe.ui.startup_setup_dialog.show(); } - // listen to csrf_update - frappe.realtime.on("csrf_generated", function(data) { - // handles the case when a user logs in again from another tab - // and it leads to invalid request in the current tab - if (data.csrf_token && data.sid===frappe.get_cookie("sid")) { - frappe.csrf_token = data.csrf_token; - } - }); - frappe.realtime.on("version-update", function() { var dialog = frappe.msgprint({ message:__("The application has been updated to a new version, please refresh this page"), diff --git a/frappe/public/js/frappe/form/controls/datetime.js b/frappe/public/js/frappe/form/controls/datetime.js index b487be6eca..c99dfe899f 100644 --- a/frappe/public/js/frappe/form/controls/datetime.js +++ b/frappe/public/js/frappe/form/controls/datetime.js @@ -17,7 +17,7 @@ frappe.ui.form.ControlDatetime = frappe.ui.form.ControlDate.extend({ set_description: function() { const { description } = this.df; const { time_zone } = frappe.sys_defaults; - if (!frappe.datetime.is_timezone_same()) { + if (!this.df.hide_timezone && !frappe.datetime.is_timezone_same()) { if (!description) { this.df.description = time_zone; } else if (!description.includes(time_zone)) { diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 603506a056..56f9430238 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -332,6 +332,10 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ let docfield = frappe.meta.get_docfield(doctype, fieldname); let label = docfield ? docfield.label : frappe.model.unscrub(fieldname); + if (docfield && docfield.fieldtype === 'Check') { + filter[3] = filter[3] ? __('Yes'): __('No'); + } + if (filter[3] && Array.isArray(filter[3]) && filter[3].length > 5) { filter[3] = filter[3].slice(0, 5); filter[3].push('...'); diff --git a/frappe/public/js/frappe/form/controls/multiselect_list.js b/frappe/public/js/frappe/form/controls/multiselect_list.js index cd86bdd767..2a7ee5cb10 100644 --- a/frappe/public/js/frappe/form/controls/multiselect_list.js +++ b/frappe/public/js/frappe/form/controls/multiselect_list.js @@ -3,7 +3,7 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({ let template = ` -
- -
+
diff --git a/frappe/public/js/frappe/form/templates/timeline_item.html b/frappe/public/js/frappe/form/templates/timeline_item.html index 1442374e24..9cb8499771 100755 --- a/frappe/public/js/frappe/form/templates/timeline_item.html +++ b/frappe/public/js/frappe/form/templates/timeline_item.html @@ -1,4 +1,6 @@ -
{% if (data.user_content) { %} - {% } else if (data.comment_type == "Energy Points") { %} + {% } else if (data.comment_type == "Energy Points" || data.template) { %} {{ data.content_html }} {% } else { %} {%= data.fullname %} @@ -200,8 +202,11 @@ {% } %} {% } %} - - – {%= data.comment_on %} + {% if (!data.template) { %} + + – {%= data.comment_on %} + + {% } %}
{% } %}
diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index b94257106e..bbe2fa2f95 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -109,9 +109,10 @@ frappe.views.BaseList = class BaseList { this.fields = this.fields.uniqBy(f => f[0] + f[1]); } - _add_field(fieldname) { + _add_field(fieldname, doctype) { if (!fieldname) return; - let doctype = this.doctype; + + if (!doctype) doctype = this.doctype; if (typeof fieldname === 'object') { // df is passed @@ -120,6 +121,8 @@ frappe.views.BaseList = class BaseList { doctype = df.parent; } + if (!this.fields) this.fields = []; + const is_valid_field = frappe.model.std_fields_list.includes(fieldname) || frappe.meta.has_field(doctype, fieldname) || fieldname === '_seen'; diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 9e1ba1b9bd..4d8121ebd6 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -528,6 +528,10 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { } get_header_html() { + if (!this.columns) { + return; + } + const subject_field = this.columns[0].df; let subject_html = ` @@ -760,26 +764,10 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { let current_count = this.data.length; let count_without_children = this.data.uniqBy(d => d.name).length; - const filters = this.get_filters_for_args(); - const with_child_table_filter = filters.some(filter => { - return filter[0] !== this.doctype; - }); - - const fields = [ - // cannot break this line as it adds extra \n's and \t's which breaks the query - `count(${with_child_table_filter ? 'distinct': ''}${frappe.model.get_full_column_name('name', this.doctype)}) AS total_count` - ]; - - return frappe.call({ - type: 'GET', - method: this.method, - args: { - doctype: this.doctype, - filters, - fields, - } - }).then(r => { - this.total_count = r.message.values[0][0] || current_count; + return frappe.db.count(this.doctype, { + filters: this.get_filters_for_args() + }).then(total_count => { + this.total_count = total_count || current_count; let str = __('{0} of {1}', [current_count, this.total_count]); if (count_without_children !== current_count) { str = __('{0} of {1} ({2} rows with children)', [count_without_children, this.total_count, current_count]); @@ -800,6 +788,12 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { return '#Form/' + this.doctype + '/' + docname; } + get_seen_class(doc) { + return JSON.parse(doc._seen || '[]').includes(frappe.session.user) + ? '' + : 'bold'; + } + get_subject_html(doc) { let user = frappe.session.user; let subject_field = this.columns[0].df; @@ -811,8 +805,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { let heart_class = liked_by.includes(user) ? 'liked-by' : 'text-extra-muted not-liked'; - const seen = JSON.parse(doc._seen || '[]') - .includes(user) ? '' : 'bold'; + const seen = this.get_seen_class(doc); let subject_html = ` @@ -1162,7 +1155,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { }); this.toggle_result_area(); this.render_list(); - if (this.$checks.length) { + if (this.$checks && this.$checks.length) { this.set_rows_as_checked(); } }); diff --git a/frappe/public/js/frappe/request.js b/frappe/public/js/frappe/request.js index 0a5f5e7f6b..5cf50bd0a3 100644 --- a/frappe/public/js/frappe/request.js +++ b/frappe/public/js/frappe/request.js @@ -126,7 +126,7 @@ frappe.request.call = function(opts) { message: __('The resource you are looking for is not available')}); }, 403: function(xhr) { - if (frappe.get_cookie('sid')==='Guest') { + if (frappe.session.user === 'Guest') { // session expired frappe.app.handle_session_expired(); } @@ -321,7 +321,7 @@ frappe.request.cleanup = function(opts, r) { if(r) { // session expired? - Guest has no business here! - if(r.session_expired || frappe.get_cookie("sid")==="Guest") { + if (r.session_expired || frappe.session.user === "Guest") { frappe.app.handle_session_expired(); return; } diff --git a/frappe/public/js/frappe/ui/filters/edit_filter.html b/frappe/public/js/frappe/ui/filters/edit_filter.html index 3908c63fa1..f6618a2107 100644 --- a/frappe/public/js/frappe/ui/filters/edit_filter.html +++ b/frappe/public/js/frappe/ui/filters/edit_filter.html @@ -10,6 +10,7 @@
+
diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index 37eab50957..4dedfb32fe 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -13,26 +13,26 @@ frappe.ui.Filter = class { set_conditions() { this.conditions = [ - ["=", __("Equals")], - ["!=", __("Not Equals")], - ["like", __("Like")], - ["not like", __("Not Like")], - ["in", __("In")], - ["not in", __("Not In")], - ["is", __("Is")], - [">", ">"], - ["<", "<"], - [">=", ">="], - ["<=", "<="], - ["Between", __("Between")], - ["Timespan", __("Timespan")], + ['=', __('Equals')], + ['!=', __('Not Equals')], + ['like', __('Like')], + ['not like', __('Not Like')], + ['in', __('In')], + ['not in', __('Not In')], + ['is', __('Is')], + ['>', '>'], + ['<', '<'], + ['>=', '>='], + ['<=', '<='], + ['Between', __('Between')], + ['Timespan', __('Timespan')], ]; this.nested_set_conditions = [ - ["descendants of", __("Descendants Of")], - ["not descendants of", __("Not Descendants Of")], - ["ancestors of", __("Ancestors Of")], - ["not ancestors of", __("Not Ancestors Of")], + ['descendants of', __('Descendants Of')], + ['not descendants of', __('Not Descendants Of')], + ['ancestors of', __('Ancestors Of')], + ['not ancestors of', __('Not Ancestors Of')], ]; this.conditions.push(...this.nested_set_conditions); @@ -42,10 +42,10 @@ frappe.ui.Filter = class { Datetime: ['like', 'not like'], Data: ['Between', 'Timespan'], Select: ['like', 'not like', 'Between', 'Timespan'], - Link: ["Between", 'Timespan', '>', '<', '>=', '<='], - Currency: ["Between", 'Timespan'], - Color: ["Between", 'Timespan'], - Check: this.conditions.map(c => c[0]).filter(c => c !== '=') + Link: ['Between', 'Timespan', '>', '<', '>=', '<='], + Currency: ['Between', 'Timespan'], + Color: ['Between', 'Timespan'], + Check: this.conditions.map((c) => c[0]).filter((c) => c !== '='), }; } @@ -57,7 +57,7 @@ frappe.ui.Filter = class { this.conditions.push([key, __(`{0}`, [filter.label])]); for (let fieldtype of Object.keys(this.invalid_condition_map)) { if (!filter.valid_for_fieldtypes.includes(fieldtype)) { - this.invalid_condition_map[fieldtype].push(filter.label); + this.invalid_condition_map[fieldtype].push(key); } } } @@ -65,10 +65,11 @@ frappe.ui.Filter = class { } make() { - this.filter_edit_area = $(frappe.render_template("edit_filter", { - conditions: this.conditions - })) - .appendTo(this.parent.find('.filter-edit-area')); + this.filter_edit_area = $( + frappe.render_template('edit_filter', { + conditions: this.conditions, + }) + ).appendTo(this.parent.find('.filter-edit-area')); this.make_select(); this.set_events(); @@ -82,41 +83,51 @@ frappe.ui.Filter = class { filter_fields: this.filter_fields, select: (doctype, fieldname) => { this.set_field(doctype, fieldname); - } + }, }); - if(this.fieldname) { + if (this.fieldname) { this.fieldselect.set_value(this.doctype, this.fieldname); } } set_events() { - this.filter_edit_area.find("a.remove-filter").on("click", () => { + this.filter_edit_area.find('a.remove-filter').on('click', () => { this.remove(); }); - this.filter_edit_area.find(".set-filter-and-run").on("click", () => { - this.filter_edit_area.removeClass("new-filter"); + this.filter_edit_area.find('.set-filter-and-run').on('click', () => { + this.filter_edit_area.removeClass('new-filter'); this.on_change(); this.update_filter_tag(); }); this.filter_edit_area.find('.condition').change(() => { - if(!this.field) return; + if (!this.field) return; let condition = this.get_condition(); let fieldtype = null; - if(["in", "like", "not in", "not like"].includes(condition)) { + if (['in', 'like', 'not in', 'not like'].includes(condition)) { fieldtype = 'Data'; this.add_condition_help(condition); + } else { + this.filter_edit_area.find('.filter-description').empty(); } - if (['Select', 'MultiSelect'].includes(this.field.df.fieldtype) && ["in", "not in"].includes(condition)) { + if ( + ['Select', 'MultiSelect'].includes(this.field.df.fieldtype) && + ['in', 'not in'].includes(condition) + ) { fieldtype = 'MultiSelect'; } - this.set_field(this.field.df.parent, this.field.df.fieldname, fieldtype, condition); + this.set_field( + this.field.df.parent, + this.field.df.fieldname, + fieldtype, + condition + ); }); } @@ -129,12 +140,12 @@ frappe.ui.Filter = class { setup_state(is_new) { let promise = Promise.resolve(); if (is_new) { - this.filter_edit_area.addClass("new-filter"); + this.filter_edit_area.addClass('new-filter'); } else { promise = this.update_filter_tag(); } - if(this.hidden) { + if (this.hidden) { promise.then(() => this.$filter_tag.hide()); } } @@ -164,13 +175,13 @@ frappe.ui.Filter = class { set_values(doctype, fieldname, condition, value) { // presents given (could be via tags!) if (this.set_field(doctype, fieldname) === false) { - return + return; } - if(this.field.df.original_type==='Check') { - value = (value==1) ? 'Yes' : 'No'; + if (this.field.df.original_type === 'Check') { + value = value == 1 ? 'Yes' : 'No'; } - if(condition) this.set_condition(condition, true); + if (condition) this.set_condition(condition, true); // set value can be asynchronous, so update_filter_tag should happen after field is set this._filter_value_set = Promise.resolve(); @@ -190,11 +201,13 @@ frappe.ui.Filter = class { set_field(doctype, fieldname, fieldtype, condition) { // set in fieldname (again) let cur = {}; - if(this.field) for(let k in this.field.df) cur[k] = this.field.df[k]; + if (this.field) for (let k in this.field.df) cur[k] = this.field.df[k]; - let original_docfield = (this.fieldselect.fields_by_name[doctype] || {})[fieldname]; + let original_docfield = (this.fieldselect.fields_by_name[doctype] || {})[ + fieldname + ]; - if(!original_docfield) { + if (!original_docfield) { console.warn(`Field ${fieldname} is not selectable.`); this.remove(); return false; @@ -214,8 +227,13 @@ frappe.ui.Filter = class { // called when condition is changed, // don't change if all is well - if(this.field && cur.fieldname == fieldname && df.fieldtype == cur.fieldtype && - df.parent == cur.parent && df.options == cur.options) { + if ( + this.field && + cur.fieldname == fieldname && + df.fieldtype == cur.fieldtype && + df.parent == cur.parent && + df.options == cur.options + ) { return; } @@ -223,20 +241,25 @@ frappe.ui.Filter = class { this.fieldselect.selected_doctype = doctype; this.fieldselect.selected_fieldname = fieldname; - if (this.filters_config && this.filters_config[condition] - && this.filters_config[condition].valid_for_fieldtypes.includes(df.fieldtype)) { + if ( + this.filters_config && + this.filters_config[condition] && + this.filters_config[condition].valid_for_fieldtypes.includes(df.fieldtype) + ) { let args = {}; if (this.filters_config[condition].depends_on) { const field_name = this.filters_config[condition].depends_on; - const filter_value = this.base_list.get_filter_value(field_name); + const filter_value = this.filter_list.get_filter_value(fieldname); args[field_name] = filter_value; } - frappe.xcall(this.filters_config[condition].get_field, args).then(field => { - df.fieldtype = field.fieldtype; - df.options = field.options; - df.fieldname = fieldname; - this.make_field(df, cur.fieldtype); - }); + frappe + .xcall(this.filters_config[condition].get_field, args) + .then(field => { + df.fieldtype = field.fieldtype; + df.options = field.options; + df.fieldname = fieldname; + this.make_field(df, cur.fieldtype); + }); } else { this.make_field(df, cur.fieldtype); } @@ -255,16 +278,18 @@ frappe.ui.Filter = class { f.refresh(); this.field = f; - if(old_text && f.fieldtype===old_fieldtype) { + if (old_text && f.fieldtype === old_fieldtype) { this.field.set_value(old_text); } // run on enter - $(this.field.wrapper).find(':input').keydown(e => { - if(e.which==13 && this.field.df.fieldtype !== 'MultiSelect') { - this.on_change(); - } - }); + $(this.field.wrapper) + .find(':input') + .keydown(e => { + if (e.which == 13 && this.field.df.fieldtype !== 'MultiSelect') { + this.on_change(); + } + }); } get_value() { @@ -273,7 +298,7 @@ frappe.ui.Filter = class { this.field.df.fieldname, this.get_condition(), this.get_selected_value(), - this.hidden + this.hidden, ]; } get_selected_value() { @@ -284,90 +309,101 @@ frappe.ui.Filter = class { return this.filter_edit_area.find('.condition').val(); } - set_condition(condition, trigger_change=false) { + set_condition(condition, trigger_change = false) { let $condition_field = this.filter_edit_area.find('.condition'); $condition_field.val(condition); - if(trigger_change) $condition_field.change(); - + if (trigger_change) $condition_field.change(); } make_tag() { if (!this.field) return; - this.$filter_tag = this.get_filter_tag_element() - .insertAfter(this.parent.find(".active-tag-filters .clear-filters")); + this.$filter_tag = this.get_filter_tag_element().insertAfter( + this.parent.find('.active-tag-filters .clear-filters') + ); this.set_filter_button_text(); this.bind_tag(); } bind_tag() { - this.$filter_tag.find(".remove-filter").on("click", this.remove.bind(this)); + this.$filter_tag.find('.remove-filter').on('click', this.remove.bind(this)); - let filter_button = this.$filter_tag.find(".toggle-filter"); - filter_button.on("click", () => { - filter_button.closest('.tag-filters-area').find('.filter-edit-area').show(); + let filter_button = this.$filter_tag.find('.toggle-filter'); + filter_button.on('click', () => { + filter_button + .closest('.tag-filters-area') + .find('.filter-edit-area') + .show(); this.filter_edit_area.toggle(); }); } set_filter_button_text() { - this.$filter_tag.find(".toggle-filter").html(this.get_filter_button_text()); + this.$filter_tag.find('.toggle-filter').html(this.get_filter_button_text()); } get_filter_button_text() { - let value = this.utils.get_formatted_value(this.field, this.get_selected_value()); - return `${__(this.field.df.label)} ${__(this.get_condition())} ${__(value)}`; + let value = this.utils.get_formatted_value( + this.field, + this.get_selected_value() + ); + return `${__(this.field.df.label)} ${__(this.get_condition())} ${__( + value + )}`; } get_filter_tag_element() { return $(`
`); } add_condition_help(condition) { - let $desc = this.field.desc_area; - if(!$desc) { - $desc = $('
').appendTo(this.field.wrapper); - } - // set description - $desc.html((in_list(["in", "not in"], condition)==="in" - ? __("values separated by commas") - : __("use % as wildcard"))+'
'); + const description = ['in', 'not in'].includes(condition) + ? __('values separated by commas') + : __('use % as wildcard'); + + this.filter_edit_area.find('.filter-description').html(description); } hide_invalid_conditions(fieldtype, original_type) { - let invalid_conditions = this.invalid_condition_map[original_type] - || this.invalid_condition_map[fieldtype] || []; + let invalid_conditions = + this.invalid_condition_map[original_type] || + this.invalid_condition_map[fieldtype] || + []; for (let condition of this.conditions) { - this.filter_edit_area.find(`.condition option[value="${condition[0]}"]`).toggle( - !invalid_conditions.includes(condition[0]) - ); + this.filter_edit_area + .find(`.condition option[value="${condition[0]}"]`) + .toggle(!invalid_conditions.includes(condition[0])); } } toggle_nested_set_conditions(df) { - let show_condition = df.fieldtype === "Link" && frappe.boot.nested_set_doctypes.includes(df.options); - this.nested_set_conditions.forEach(condition => { - this.filter_edit_area.find(`.condition option[value="${condition[0]}"]`).toggle(show_condition); + let show_condition = + df.fieldtype === 'Link' && + frappe.boot.nested_set_doctypes.includes(df.options); + this.nested_set_conditions.forEach((condition) => { + this.filter_edit_area + .find(`.condition option[value="${condition[0]}"]`) + .toggle(show_condition); }); } }; frappe.ui.filter_utils = { get_formatted_value(field, value) { - if(field.df.fieldname==="docstatus") { - value = {0:"Draft", 1:"Submitted", 2:"Cancelled"}[value] || value; - } else if(field.df.original_type==="Check") { - value = {0:"No", 1:"Yes"}[cint(value)]; + if (field.df.fieldname === 'docstatus') { + value = { 0: 'Draft', 1: 'Submitted', 2: 'Cancelled' }[value] || value; + } else if (field.df.original_type === 'Check') { + value = { 0: 'No', 1: 'Yes' }[cint(value)]; } - return frappe.format(value, field.df, {only_value: 1}); + return frappe.format(value, field.df, { only_value: 1 }); }, get_selected_value(field, condition) { @@ -382,7 +418,7 @@ frappe.ui.filter_utils = { } if (field.df.original_type == 'Check') { - val = (val=='Yes' ? 1 :0); + val = val == 'Yes' ? 1 : 0; } if (condition.indexOf('like', 'not like') !== -1) { @@ -390,12 +426,13 @@ frappe.ui.filter_utils = { if (val && !(val.startsWith('%') || val.endsWith('%'))) { val = '%' + val + '%'; } - } else if (in_list(["in", "not in"], condition)) { + } else if (in_list(['in', 'not in'], condition)) { if (val) { - val = val.split(',').map(v => strip(v)); + val = val.split(',').map((v) => strip(v)); } - } if (val === '%') { - val = ""; + } + if (val === '%') { + val = ''; } return val; @@ -404,7 +441,7 @@ frappe.ui.filter_utils = { get_default_condition(df) { if (df.fieldtype == 'Data') { return 'like'; - } else if (df.fieldtype == 'Date' || df.fieldtype == 'Datetime'){ + } else if (df.fieldtype == 'Date' || df.fieldtype == 'Datetime') { return 'Between'; } else { return '='; @@ -413,44 +450,73 @@ frappe.ui.filter_utils = { set_fieldtype(df, fieldtype, condition) { // reset - if(df.original_type) - df.fieldtype = df.original_type; - else - df.original_type = df.fieldtype; + if (df.original_type) df.fieldtype = df.original_type; + else df.original_type = df.fieldtype; - df.description = ''; df.reqd = 0; + df.description = ''; + df.reqd = 0; df.ignore_link_validation = true; // given - if(fieldtype) { + if (fieldtype) { df.fieldtype = fieldtype; return; } // scrub - if(df.fieldname=="docstatus") { - df.fieldtype="Select", - df.options=[ - {value:0, label:__("Draft")}, - {value:1, label:__("Submitted")}, - {value:2, label:__("Cancelled")} + if (df.fieldname == 'docstatus') { + df.fieldtype = 'Select', + df.options = [ + { value: 0, label: __('Draft') }, + { value: 1, label: __('Submitted') }, + { value: 2, label: __('Cancelled') }, ]; - } else if(df.fieldtype=='Check') { - df.fieldtype='Select'; - df.options='No\nYes'; - } else if(['Text','Small Text','Text Editor','Code','Tag','Comments', - 'Dynamic Link','Read Only','Assign'].indexOf(df.fieldtype)!=-1) { + } else if (df.fieldtype == 'Check') { + df.fieldtype = 'Select'; + df.options = 'No\nYes'; + } else if ( + [ + 'Text', + 'Small Text', + 'Text Editor', + 'Code', + 'Tag', + 'Comments', + 'Dynamic Link', + 'Read Only', + 'Assign', + ].indexOf(df.fieldtype) != -1 + ) { df.fieldtype = 'Data'; - } else if(df.fieldtype=='Link' && ['=', '!=', 'descendants of', 'ancestors of', 'not descendants of', 'not ancestors of'].indexOf(condition)==-1) { + } else if ( + df.fieldtype == 'Link' && + [ + '=', + '!=', + 'descendants of', + 'ancestors of', + 'not descendants of', + 'not ancestors of', + ].indexOf(condition) == -1 + ) { df.fieldtype = 'Data'; } - if(df.fieldtype==="Data" && (df.options || "").toLowerCase()==="email") { + if ( + df.fieldtype === 'Data' && + (df.options || '').toLowerCase() === 'email' + ) { df.options = null; } - if(condition == "Between" && (df.fieldtype == 'Date' || df.fieldtype == 'Datetime')){ + if ( + condition == 'Between' && + (df.fieldtype == 'Date' || df.fieldtype == 'Datetime') + ) { df.fieldtype = 'DateRange'; } - if (condition == 'Timespan' && ['Date', 'Datetime', 'DateRange', 'Select'].includes(df.fieldtype)) { + if ( + condition == 'Timespan' && + ['Date', 'Datetime', 'DateRange', 'Select'].includes(df.fieldtype) + ) { df.fieldtype = 'Select'; df.options = this.get_timespan_options(['Last', 'Today', 'This', 'Next']); } @@ -466,15 +532,15 @@ frappe.ui.filter_utils = { get_timespan_options(periods) { const period_map = { - 'Last': ['Week', 'Month', 'Quarter', '6 months', 'Year'], - 'Today': null, - 'This': ['Week', 'Month', 'Quarter', 'Year'], - 'Next': ['Week', 'Month', 'Quarter', '6 months', 'Year'] + Last: ['Week', 'Month', 'Quarter', '6 months', 'Year'], + Today: null, + This: ['Week', 'Month', 'Quarter', 'Year'], + Next: ['Week', 'Month', 'Quarter', '6 months', 'Year'], }; let options = []; - periods.forEach(period => { + periods.forEach((period) => { if (period_map[period]) { - period_map[period].forEach(p => { + period_map[period].forEach((p) => { options.push({ label: __(`{0} {1}`, [period, p]), value: `${period.toLowerCase()} ${p.toLowerCase()}`, @@ -488,5 +554,5 @@ frappe.ui.filter_utils = { } }); return options; - } + }, }; diff --git a/frappe/public/js/frappe/ui/filters/filter_list.js b/frappe/public/js/frappe/ui/filters/filter_list.js index ed9ddefe64..6c577aa0bc 100644 --- a/frappe/public/js/frappe/ui/filters/filter_list.js +++ b/frappe/public/js/frappe/ui/filters/filter_list.js @@ -104,13 +104,18 @@ frappe.ui.FilterGroup = class { filter_items: (doctype, fieldname) => { return !this.filter_exists([doctype, fieldname]); }, - base_list: this.base_list + filter_list: this.base_list || this, }; let filter = new frappe.ui.Filter(args); this.filters.push(filter); return filter; } + get_filter_value(fieldname) { + let filter_obj = this.filters.find(f => f.fieldname == fieldname) || {}; + return filter_obj.value; + } + filter_exists(filter_value) { // filter_value of form: [doctype, fieldname, condition, value] let exists = false; diff --git a/frappe/public/js/frappe/ui/filters/filters.js b/frappe/public/js/frappe/ui/filters/filters.js deleted file mode 100644 index a775413d39..0000000000 --- a/frappe/public/js/frappe/ui/filters/filters.js +++ /dev/null @@ -1,684 +0,0 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -// MIT License. See license.txt - -frappe.ui.FilterList = Class.extend({ - init: function(opts) { - $.extend(this, opts); - this.filters = []; - this.wrapper = this.parent; - this.stats = []; - this.make(); - this.set_events(); - }, - make: function() { - this.wrapper.find('.show_filters, .filter_area').remove(); - this.wrapper.append(` -
-
- -
-
-
`); - }, - set_events: function() { - var me = this; - // show filters - this.wrapper.find('.new-filter').bind('click', function() { - me.add_filter(); - }); - - this.wrapper.find('.clear-filters').bind('click', function() { - me.clear_filters(); - $('.date-range-picker').val('') - me.base_list.run(); - $(this).addClass("hide"); - }); - }, - - show_filters: function() { - this.wrapper.find('.show_filters').toggle(); - if(!this.filters.length) { - this.add_filter(this.doctype, 'name'); - this.filters[0].wrapper.find(".filter_field input").focus(); - } - }, - - clear_filters: function() { - $.each(this.filters, function(i, f) { f.remove(true); }); - if(this.base_list.page.fields_dict) { - $.each(this.base_list.page.fields_dict, (key, value) => { - value.set_input(''); - }); - } - this.filters = []; - }, - - add_filter: function(doctype, fieldname, condition, value, hidden) { - // adds a new filter, returns true if filter has been added - - // allow equal to be used as like - let base_filter = this.base_list.page.fields_dict[fieldname]; - if (base_filter - && (base_filter.df.condition==condition - || (condition==='=' && base_filter.df.condition==='like'))) { - // if filter exists in base_list, then exit - this.base_list.page.fields_dict[fieldname].set_input(value); - - return true; - } - - if(doctype && fieldname - && !frappe.meta.has_field(doctype, fieldname) - && !in_list(frappe.model.std_fields_list, fieldname)) { - frappe.msgprint({ - message: __('Filter {0} missing', [fieldname.bold()]), - title: 'Invalid Filter', - indicator: 'red' - }); - return false; - } - - this.wrapper.find('.show_filters').toggle(true); - var is_new_filter = arguments.length===0; - - if (is_new_filter && this.wrapper.find(".is-new-filter:visible").length) { - // only allow 1 new filter at a time! - return false; - } - - var filter = this.push_new_filter(doctype, fieldname, condition, value); - if (!filter) return; - - if(this.wrapper.find('.clear-filters').hasClass("hide")) { - this.wrapper.find('.clear-filters').removeClass("hide"); - } - - if (filter && is_new_filter) { - filter.wrapper.addClass("is-new-filter"); - } else { - filter.freeze(); - } - - if (hidden) { - filter.$btn_group.addClass("hide"); - } - - return true; - }, - push_new_filter: function(doctype, fieldname, condition, value) { - if(this.filter_exists(doctype, fieldname, condition, value)) { - return; - } - - // if standard filter exists, then clear it. - if(this.base_list.page.fields_dict[fieldname]) { - this.base_list.page.fields_dict[fieldname].set_input(''); - } - - var filter = new frappe.ui.Filter({ - flist: this, - _doctype: doctype, - fieldname: fieldname, - condition: condition, - value: value - }); - - this.filters.push(filter); - - return filter; - }, - - remove: function(filter) { - // remove `filter` from flist - for (var i in this.filters) { - if (this.filters[i] === filter) { - break; - } - } - if (i!==undefined) { - // remove index - this.filters.splice(i, 1); - } - }, - - filter_exists: function(doctype, fieldname, condition, value) { - var flag = false; - for(var i in this.filters) { - if(this.filters[i].field) { - var f = this.filters[i].get_value(); - - if(f[0]==doctype && f[1]==fieldname && f[2]==condition && f[3]==value) { - flag = true; - } else if($.isArray(value) && frappe.utils.arrays_equal(value, f[3])) { - flag = true; - } - } - } - return flag; - }, - - get_filters: function() { - // get filter values as dict - var values = []; - $.each(this.filters, function(i, filter) { - if(filter.field) { - filter.freeze(); - values.push(filter.get_value()); - } - }); - this.base_list.update_standard_filters(values); - - return values; - }, - - // remove hidden filters - update_filters: function() { - var fl = []; - $.each(this.filters, function(i, f) { - if(f.field) fl.push(f); - }) - this.filters = fl; - if(this.filters.length === 0) { - this.wrapper.find('.clear-filters').addClass("hide"); - } - }, - - get_filter: function(fieldname) { - for(var i in this.filters) { - if(this.filters[i].field && this.filters[i].field.df.fieldname==fieldname) - return this.filters[i]; - } - }, - - get_formatted_value: function(field, val){ - var value = val; - - if(field.df.fieldname==="docstatus") { - value = {0:"Draft", 1:"Submitted", 2:"Cancelled"}[value] || value; - } else if(field.df.original_type==="Check") { - value = {0:"No", 1:"Yes"}[cint(value)]; - } else if (field.df.original_type === "Duration") { - let duration_options = { - hide_days: field.df.hide_days, - hide_seconds: field.df.hide_seconds - }; - value = frappe.utils.get_formatted_duration(value, duration_options); - } - - value = frappe.format(value, field.df, {only_value: 1}); - return value; - } -}); - -frappe.ui.Filter = Class.extend({ - init: function(opts) { - $.extend(this, opts); - - this.doctype = this.flist.doctype; - this.make(); - this.make_select(); - this.set_events(); - }, - make: function() { - this.wrapper = $(frappe.render_template("edit_filter", {})) - .appendTo(this.flist.wrapper.find('.filter_area')); - }, - make_select: function() { - var me = this; - this.fieldselect = new frappe.ui.FieldSelect({ - parent: this.wrapper.find('.fieldname_select_area'), - doctype: this.doctype, - filter_fields: this.filter_fields, - select: function(doctype, fieldname) { - me.set_field(doctype, fieldname); - } - }); - if(this.fieldname) { - this.fieldselect.set_value(this._doctype || this.doctype, this.fieldname); - } - }, - set_events: function() { - var me = this; - - this.wrapper.find("a.remove-filter").on("click", function() { - me.remove(); - }); - - this.wrapper.find(".set-filter-and-run").on("click", function() { - me.wrapper.removeClass("is-new-filter"); - me.flist.base_list.run(); - me.apply(); - }); - - // add help for "in" codition - me.wrapper.find('.condition').change(function() { - if(!me.field) return; - var condition = $(this).val(); - if(in_list(["in", "like", "not in", "not like"], condition)) { - me.set_field(me.field.df.parent, me.field.df.fieldname, 'Data', condition); - if(!me.field.desc_area) { - me.field.desc_area = $('
').appendTo(me.field.wrapper); - } - // set description - me.field.desc_area.html((in_list(["in", "not in"], condition)==="in" - ? __("values separated by commas") - : __("use % as wildcard"))+'
'); - } else { - //if condition selected after refresh - me.set_field(me.field.df.parent, me.field.df.fieldname, null, condition); - } - }); - - // set the field - if(me.fieldname) { - // pre-sets given (could be via tags!) - return this.set_values(me._doctype, me.fieldname, me.condition, me.value); - } else { - me.set_field(me.doctype, 'name'); - } - }, - - apply: function() { - var f = this.get_value(); - - this.flist.remove(this); - this.flist.push_new_filter(f[0], f[1], f[2], f[3]); - this.remove(); - }, - - remove: function(dont_run) { - this.wrapper.remove(); - this.$btn_group && this.$btn_group.remove(); - this.field = null; - this.flist.update_filters(); - - if(!dont_run) { - this.flist.base_list.refresh(true); - } - }, - - set_values: function(doctype, fieldname, condition, value) { - // presents given (could be via tags!) - this.set_field(doctype, fieldname); - - // change 0,1 to Yes, No for check field type - if(this.field.df.original_type==='Check') { - if(value==0) value = 'No'; - else if(value==1) value = 'Yes'; - } - - if(condition) { - this.wrapper.find('.condition').val(condition).change(); - } - if(value!=null) { - return this.field.set_value(value); - } - }, - - set_field: function(doctype, fieldname, fieldtype, condition) { - var me = this; - - // set in fieldname (again) - var cur = me.field ? { - fieldname: me.field.df.fieldname, - fieldtype: me.field.df.fieldtype, - parent: me.field.df.parent, - } : {}; - - var original_docfield = me.fieldselect.fields_by_name[doctype][fieldname]; - if(!original_docfield) { - frappe.msgprint(__("Field {0} is not selectable.", [fieldname])); - return; - } - - var df = copy_dict(me.fieldselect.fields_by_name[doctype][fieldname]); - - // filter field shouldn't be read only or hidden - df.read_only = 0; - df.hidden = 0; - - if(!condition) this.set_default_condition(df, fieldtype); - this.set_fieldtype(df, fieldtype); - - // called when condition is changed, - // don't change if all is well - if(me.field && cur.fieldname == fieldname && df.fieldtype == cur.fieldtype && - df.parent == cur.parent) { - return; - } - - // clear field area and make field - me.fieldselect.selected_doctype = doctype; - me.fieldselect.selected_fieldname = fieldname; - - // save old text - var old_text = null; - if(me.field) { - old_text = me.field.get_value(); - } - - var field_area = me.wrapper.find('.filter_field').empty().get(0); - var f = frappe.ui.form.make_control({ - df: df, - parent: field_area, - only_input: true, - }) - f.refresh(); - - me.field = f; - if(old_text && me.field.df.fieldtype===cur.fieldtype) { - me.field.set_value(old_text); - } - - // run on enter - $(me.field.wrapper).find(':input').keydown(function(ev) { - if(ev.which==13) { - me.flist.base_list.run(); - } - }) - }, - - set_fieldtype: function(df, fieldtype) { - // reset - if(df.original_type) - df.fieldtype = df.original_type; - else - df.original_type = df.fieldtype; - - df.description = ''; df.reqd = 0; - df.ignore_link_validation = true; - - // given - if(fieldtype) { - df.fieldtype = fieldtype; - return; - } - - // scrub - if(df.fieldname=="docstatus") { - df.fieldtype="Select", - df.options=[ - {value:0, label:__("Draft")}, - {value:1, label:__("Submitted")}, - {value:2, label:__("Cancelled")} - ] - } else if(df.fieldtype=='Check') { - df.fieldtype='Select'; - df.options='No\nYes'; - } else if(['Text','Small Text','Text Editor','Code','Tag','Comments', - 'Dynamic Link','Read Only','Assign'].indexOf(df.fieldtype)!=-1) { - df.fieldtype = 'Data'; - } else if(df.fieldtype=='Link' && ['=', '!='].indexOf(this.wrapper.find('.condition').val())==-1) { - df.fieldtype = 'Data'; - } - if(df.fieldtype==="Data" && (df.options || "").toLowerCase()==="email") { - df.options = null; - } - if(this.wrapper.find('.condition').val()== "Between" && (df.fieldtype == 'Date' || df.fieldtype == 'Datetime')){ - df.fieldtype = 'DateRange'; - } - }, - - set_default_condition: function(df, fieldtype) { - if(!fieldtype) { - // set as "like" for data fields - if (df.fieldtype == 'Data') { - this.wrapper.find('.condition').val('like'); - } else if (df.fieldtype == 'Date' || df.fieldtype == 'Datetime'){ - this.wrapper.find('.condition').val('Between'); - }else{ - this.wrapper.find('.condition').val('='); - } - } - }, - - get_value: function() { - return [this.fieldselect.selected_doctype, - this.field.df.fieldname, this.get_condition(), this.get_selected_value()]; - }, - - get_selected_value: function() { - var val = this.field.get_value(); - - if(typeof val==='string') { - val = strip(val); - } - - if(this.field.df.original_type == 'Check') { - val = (val=='Yes' ? 1 :0); - } - - if(this.get_condition().indexOf('like', 'not like')!==-1) { - // automatically append wildcards - if(val) { - if(val.slice(0,1) !== "%") { - val = "%" + val; - } - if(val.slice(-1) !== "%") { - val = val + "%"; - } - } - } else if(in_list(["in", "not in"], this.get_condition())) { - if(val) { - val = $.map(val.split(","), function(v) { return strip(v); }); - } - } if(val === '%') { - val = ""; - } - - return val; - }, - - get_condition: function() { - return this.wrapper.find('.condition').val(); - }, - - freeze: function() { - if(this.$btn_group) { - // already made, just hide the condition setter - this.set_filter_button_text(); - this.wrapper.toggle(false); - return; - } - - var me = this; - - // add a button for new filter if missing - this.$btn_group = $(`
- -
`) - .insertAfter(this.flist.wrapper.find(".set-filters .new-filter")); - - this.set_filter_button_text(); - - this.$btn_group.find(".remove-filter").on("click", function() { - me.remove(); - }); - - this.$btn_group.find(".toggle-filter").on("click", function() { - $(this).closest('.show_filters').find('.filter_area').show(); - me.wrapper.toggle(); - }) - this.wrapper.toggle(false); - }, - - set_filter_button_text: function() { - var value = this.get_selected_value(); - value = this.flist.get_formatted_value(this.field, value); - - // for translations - // __("like"), __("not like"), __("in") - - this.$btn_group.find(".toggle-filter") - .html(repl('%(label)s %(condition)s "%(value)s"', { - label: __(this.field.df.label), - condition: __(this.get_condition()), - value: __(value), - })); - } - -}); - -// ') - .appendTo(this.parent) - .on("click", function () { $(this).select(); }); - this.select_input = this.$select.get(0); - this.awesomplete = new Awesomplete(this.select_input, { - minChars: 0, - maxItems: 99, - autoFirst: true, - list: me.options, - item: function(item, input) { - return $(repl('
  • %(label)s

  • ', item)) - .data("item.autocomplete", item) - .get(0); - } - }); - this.$select.on("awesomplete-select", function(e) { - var o = e.originalEvent; - var value = o.text.value; - var item = me.awesomplete.get_item(value); - me.selected_doctype = item.doctype; - me.selected_fieldname = item.fieldname; - if(me.select) me.select(item.doctype, item.fieldname); - }); - this.$select.on("awesomplete-selectcomplete", function(e) { - var o = e.originalEvent; - var value = o.text.value; - var item = me.awesomplete.get_item(value); - me.$select.val(item.label); - }); - - if(this.filter_fields) { - for(var i in this.filter_fields) - this.add_field_option(this.filter_fields[i]) - } else { - this.build_options(); - } - this.set_value(this.doctype, "name"); - window.last_filter = this; - }, - get_value: function() { - return this.selected_doctype ? this.selected_doctype + "." + this.selected_fieldname : null; - }, - val: function(value) { - if(value===undefined) { - return this.get_value(); - } else { - this.set_value(value); - } - }, - clear: function() { - this.selected_doctype = null; - this.selected_fieldname = null; - this.$select.val(""); - }, - set_value: function(doctype, fieldname) { - var me = this; - this.clear(); - if(!doctype) return; - - // old style - if(doctype.indexOf(".")!==-1) { - var parts = doctype.split("."); - doctype = parts[0]; - fieldname = parts[1]; - } - - $.each(this.options, function(i, v) { - if(v.doctype===doctype && v.fieldname===fieldname) { - me.selected_doctype = doctype; - me.selected_fieldname = fieldname; - me.$select.val(v.label); - return false; - } - }); - }, - build_options: function() { - var me = this; - me.table_fields = []; - var std_filters = $.map(frappe.model.std_fields, function(d) { - var opts = {parent: me.doctype} - if(d.fieldname=="name") opts.options = me.doctype; - return $.extend(copy_dict(d), opts); - }); - - // add parenttype column - var doctype_obj = locals['DocType'][me.doctype]; - if(doctype_obj && cint(doctype_obj.istable)) { - std_filters = std_filters.concat([{ - fieldname: 'parent', - fieldtype: 'Data', - label: 'Parent', - parent: me.doctype, - }]); - } - - // blank - if(this.with_blank) { - this.options.push({ - label:"", - value:"", - }) - } - - // main table - var main_table_fields = std_filters.concat(frappe.meta.docfield_list[me.doctype]); - $.each(frappe.utils.sort(main_table_fields, "label", "string"), function(i, df) { - // show fields where user has read access and if report hide flag is not set - if(frappe.perm.has_perm(me.doctype, df.permlevel, "read") && !df.report_hide) - me.add_field_option(df); - }); - - // child tables - $.each(me.table_fields, function(i, table_df) { - if(table_df.options) { - var child_table_fields = [].concat(frappe.meta.docfield_list[table_df.options]); - $.each(frappe.utils.sort(child_table_fields, "label", "string"), function(i, df) { - // show fields where user has read access and if report hide flag is not set - if(frappe.perm.has_perm(me.doctype, df.permlevel, "read") && !df.report_hide) - me.add_field_option(df); - }); - } - }); - }, - - add_field_option: function(df) { - var me = this; - if(me.doctype && df.parent==me.doctype) { - var label = __(df.label); - var table = me.doctype; - if(frappe.model.table_fields.includes(df.fieldtype)) me.table_fields.push(df); - } else { - var label = __(df.label) + ' (' + __(df.parent) + ')'; - var table = df.parent; - } - if(frappe.model.no_value_type.indexOf(df.fieldtype) == -1 && - !(me.fields_by_name[df.parent] && me.fields_by_name[df.parent][df.fieldname])) { - this.options.push({ - label: label, - value: table + "." + df.fieldname, - fieldname: df.fieldname, - doctype: df.parent - }); - if(!me.fields_by_name[df.parent]) me.fields_by_name[df.parent] = {}; - me.fields_by_name[df.parent][df.fieldname] = df; - } - }, -}) diff --git a/frappe/public/js/frappe/ui/notifications/notifications.js b/frappe/public/js/frappe/ui/notifications/notifications.js index 3570420c81..03f3662d2a 100644 --- a/frappe/public/js/frappe/ui/notifications/notifications.js +++ b/frappe/public/js/frappe/ui/notifications/notifications.js @@ -309,7 +309,7 @@ frappe.ui.Notifications = class Notifications { let mark_read_action = field.read ? '': 'data-action="mark_as_read"'; let message = field.subject; let title = message.match(/(.*?)<\/b>/); - message = title ? message.replace(title[1], frappe.ellipsis(title[1], 100)): message; + message = title ? message.replace(title[1], frappe.ellipsis(strip_html(title[1]), 100)) : message; let message_html = `
    ${message}
    `; let user = field.from_user; let user_avatar = frappe.avatar(user, 'avatar-small user-avatar'); diff --git a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js index 3e59986928..e11adcfb66 100644 --- a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js +++ b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js @@ -228,7 +228,7 @@ frappe.search.AwesomeBar = Class.extend({ } this.options.push({ - label: __("Search for '{0}'", [txt.bold()]), + label: __("Search for '{0}'", [frappe.utils.xss_sanitise(txt).bold()]), value: __("Search for '{0}'", [txt]), match: txt, index: 100, diff --git a/frappe/public/js/frappe/utils/dashboard_utils.js b/frappe/public/js/frappe/utils/dashboard_utils.js index d1621a3e15..fca26d6ece 100644 --- a/frappe/public/js/frappe/utils/dashboard_utils.js +++ b/frappe/public/js/frappe/utils/dashboard_utils.js @@ -47,7 +47,7 @@ frappe.dashboard_utils = { frappe.dom.eval(config); return frappe.dashboards.chart_sources[chart.source].filters; }); - } else if (chart.chart_type === 'Report') { + } else if (chart.chart_type === 'Report' && chart.report_name) { return frappe.report_utils.get_report_filters(chart.report_name).then(filters => { return filters; }); @@ -97,6 +97,157 @@ frappe.dashboard_utils = { get_year(date_str) { return date_str.substring(0, date_str.indexOf('-')); + }, + + remove_common_static_filter_values(static_filters, dynamic_filters) { + if (dynamic_filters) { + if ($.isArray(static_filters)) { + static_filters = static_filters.filter(static_filter => { + for (let dynamic_filter of dynamic_filters) { + if (static_filter[0] == dynamic_filter[0] + && static_filter[1] == dynamic_filter[1]) { + return false; + } + } + return true; + }); + } else { + for (let key of Object.keys(dynamic_filters)) { + delete static_filters[key]; + } + } + } + + return static_filters; + }, + + get_fields_for_dynamic_filter_dialog(is_document_type, filters, dynamic_filters) { + let fields = [ + { + fieldtype: 'HTML', + fieldname: 'description', + options: + `
    +

    Set dynamic filter values in JavaScript for the required fields here. +

    +

    Ex: + frappe.defaults.get_user_default("Company") +

    +
    ` + } + ]; + + if (is_document_type) { + if (dynamic_filters) { + filters = [...filters, ...dynamic_filters]; + } + filters.forEach(f => { + for (let field of fields) { + if (field.fieldname == f[0] + ':' + f[1]) { + return; + } + } + if (f[2] == '=') { + fields.push({ + label: `${f[1]} (${f[0]})`, + fieldname: f[0] + ':' + f[1], + fieldtype: 'Data', + }); + } + }); + } else { + filters = {...dynamic_filters, ...filters}; + for (let key of Object.keys(filters)) { + fields.push({ + label: key, + fieldname: key, + fieldtype: 'Data', + }); + } + } + + return fields; + }, + + get_all_filters(doc) { + let filters = JSON.parse(doc.filters_json || "null"); + let dynamic_filters = JSON.parse(doc.dynamic_filters_json || "null"); + + if (!dynamic_filters) { + return filters; + } + + if ($.isArray(dynamic_filters)) { + dynamic_filters.forEach(f => { + try { + f[3] = eval(f[3]); + } catch (e) { + frappe.throw(__(`Invalid expression set in filter ${f[1]} (${f[0]})`)); + } + }); + filters = [...filters, ...dynamic_filters]; + } else { + for (let key of Object.keys(dynamic_filters)) { + try { + const val = eval(dynamic_filters[key]); + dynamic_filters[key] = val; + } catch (e) { + frappe.throw(__(`Invalid expression set in filter ${key}`)); + } + } + Object.assign(filters, dynamic_filters); + } + + return filters; + }, + + get_dashboard_link_field() { + let field = { + label: __('Select Dashboard'), + fieldtype: 'Link', + fieldname: 'dashboard', + options: 'Dashboard', + }; + + if (!frappe.boot.developer_mode) { + field.get_query = () => { + return { + filters: { + is_standard: 0 + } + }; + }; + } + + return field; + }, + + get_add_to_dashboard_dialog(docname, doctype, method) { + const field = this.get_dashboard_link_field(); + + const dialog = new frappe.ui.Dialog({ + title: __('Add to Dashboard'), + fields: [field], + primary_action: (values) => { + values.name = docname; + values.set_standard = frappe.boot.developer_mode; + frappe.xcall( + method, + {args: values} + ).then(()=> { + let dashboard_route_html = + `${values.dashboard}`; + let message = + __(`${doctype} ${values.name} added to Dashboard ` + dashboard_route_html); + + frappe.msgprint(message); + }); + + dialog.hide(); + } + }); + + return dialog; } }; \ No newline at end of file diff --git a/frappe/public/js/frappe/utils/help.js b/frappe/public/js/frappe/utils/help.js index 336d1ef526..ec6f7c8158 100644 --- a/frappe/public/js/frappe/utils/help.js +++ b/frappe/public/js/frappe/utils/help.js @@ -17,7 +17,7 @@ frappe.help.show = function(doctype) { frappe.help.show_video = function(youtube_id, title) { if (frappe.utils.is_url(youtube_id)) { - const expression = '(?:youtube.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu.be/)([^\"&?/s]{11})'; + const expression = '(?:youtube.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu.be/)([^"&?\\s]{11})'; youtube_id = youtube_id.match(expression)[1]; } diff --git a/frappe/public/js/frappe/views/breadcrumbs.js b/frappe/public/js/frappe/views/breadcrumbs.js index 0058310e3f..dbe591f6fd 100644 --- a/frappe/public/js/frappe/views/breadcrumbs.js +++ b/frappe/public/js/frappe/views/breadcrumbs.js @@ -6,7 +6,6 @@ frappe.breadcrumbs = { preferred: { "File": "", - "Video": "", "Dashboard": "Customization", "Dashboard Chart": "Customization", "Dashboard Chart Source": "Customization" diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index 8dad5d9121..29b21242af 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -44,26 +44,6 @@ frappe.views.CommunicationComposer = Class.extend({ } }); - $(document).on("upload_complete", function(event, attachment) { - if(me.dialog.display) { - var wrapper = $(me.dialog.fields_dict.select_attachments.wrapper); - - // find already checked items - var checked_items = wrapper.find('[data-file-name]:checked').map(function() { - return $(this).attr("data-file-name"); - }); - - // reset attachment list - me.render_attach(); - - // check latest added - checked_items.push(attachment.name); - - $.each(checked_items, function(i, filename) { - wrapper.find('[data-file-name="'+ filename +'"]').prop("checked", true); - }); - } - }) this.prepare(); this.dialog.show(); @@ -174,17 +154,21 @@ frappe.views.CommunicationComposer = Class.extend({ } if (!this.subject) { - if (this.frm.subject_field && this.frm.doc[this.frm.subject_field]) { - this.subject = __("Re: {0}", [this.frm.doc[this.frm.subject_field]]); - } else { - let title = this.frm.doc.name; - if(this.frm.meta.title_field && this.frm.doc[this.frm.meta.title_field] - && this.frm.doc[this.frm.meta.title_field] != this.frm.doc.name) { - title = `${this.frm.doc[this.frm.meta.title_field]} (#${this.frm.doc.name})`; - } - this.subject = `${__(this.frm.doctype)}: ${title}`; + this.subject = this.frm.doc.name; + if (this.frm.meta.subject_field && this.frm.doc[this.frm.meta.subject_field]) { + this.subject = this.frm.doc[this.frm.meta.subject_field]; + } else if (this.frm.meta.title_field && this.frm.doc[this.frm.meta.title_field]) { + this.subject = this.frm.doc[this.frm.meta.title_field]; } } + + // always add an identifier to catch a reply + // some email clients (outlook) may not send the message id to identify + // the thread. So as a backup we use the name of the document as identifier + let identifier = `#${this.frm.doc.name}`; + if (!this.subject.includes(identifier)) { + this.subject = `${this.subject} (${identifier})`; + } } if (this.frm && !this.recipients) { @@ -383,77 +367,86 @@ frappe.views.CommunicationComposer = Class.extend({ folder: 'Home/Attachments', on_success: attachment => { this.attachments.push(attachment); - this.render_attach(); + this.render_attachment_rows(attachment); } }; - if(this.frm) { + if (this.frm) { args = { doctype: this.frm.doctype, docname: this.frm.docname, folder: 'Home/Attachments', on_success: attachment => { this.frm.attachments.attachment_uploaded(attachment); - this.render_attach(); + this.render_attachment_rows(attachment); } - } + }; } - $("
    " - +__("Select Attachments")+"
    \ -

    \ - " - +__("Add Attachment")+"

    ").appendTo(attach.empty()) + $(` +
    + ${__("Select Attachments")} +
    +
    +

    + + + ${__("Add Attachment")} + +

    + `).appendTo(attach.empty()); + attach .find(".add-more-attachments a") - .on('click',() => new frappe.ui.FileUploader(args)); - this.render_attach(); + .on('click', () => new frappe.ui.FileUploader(args)); + this.render_attachment_rows(); }, - render_attach:function(){ - var fields = this.dialog.fields_dict; - var attach = $(fields.select_attachments.wrapper).find(".attach-list").empty(); - var files = []; - if (this.attachments && this.attachments.length) { - files = files.concat(this.attachments); - } - if (cur_frm) { - files = files.concat(cur_frm.get_files()); - } + render_attachment_rows: function(attachment) { + const select_attachments = this.dialog.fields_dict.select_attachments; + const attachment_rows = $(select_attachments.wrapper).find(".attach-list"); + if (attachment) { + attachment_rows.append(this.get_attachment_row(attachment, true)); + } else { + let files = []; + if (this.attachments && this.attachments.length) { + files = files.concat(this.attachments); + } + if (this.frm) { + files = files.concat(this.frm.get_files()); + } - if(files.length) { - $.each(files, function(i, f) { - if (!f.file_name) return; - f.file_url = frappe.urllib.get_full_url(f.file_url); - - $(repl('

    ' - + '

    ', f)) - .appendTo(attach) - }); - } - this.select_attachments(); - }, - select_attachments:function(){ - let me = this; - if(me.dialog.display) { - let wrapper = $(me.dialog.fields_dict.select_attachments.wrapper); - - let unchecked_items = wrapper.find('[data-file-name]:not(:checked)').map(function() { - return $(this).attr("data-file-name"); - }); - - $.each(unchecked_items, function(i, filename) { - wrapper.find('[data-file-name="'+ filename +'"]').prop("checked", true); - }); + if (files.length) { + $.each(files, (i, f) => { + if (!f.file_name) return; + if (!attachment_rows.find(`[data-file-name="${f.name}"]`).length) { + f.file_url = frappe.urllib.get_full_url(f.file_url); + attachment_rows.append(this.get_attachment_row(f)); + } + }); + } } }, + + get_attachment_row(attachment, checked) { + return $(`

    + + + +

    `); + }, + setup_email: function() { // email - var me = this; var fields = this.dialog.fields_dict; if(this.attach_document_print) { diff --git a/frappe/public/js/frappe/views/dashboard/dashboard_view.js b/frappe/public/js/frappe/views/dashboard/dashboard_view.js index 83f45da5be..6f6279fd08 100644 --- a/frappe/public/js/frappe/views/dashboard/dashboard_view.js +++ b/frappe/public/js/frappe/views/dashboard/dashboard_view.js @@ -44,10 +44,6 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
    ${dashboard_name}
    -
    ${__('Customize')}
    `); }; @@ -105,9 +106,9 @@ export default class Desktop { }; this.sidebar_categories.forEach(category => { - if (this.desktop_settings.hasOwnProperty(category)) { + if (this.sidebar_configuration.hasOwnProperty(category)) { make_category_title(category); - this.desktop_settings[category].forEach(item => { + this.sidebar_configuration[category].forEach(item => { make_sidebar_category_item(item); }); } @@ -139,8 +140,8 @@ export default class Desktop { } get_page_to_show() { - const default_page = this.desktop_settings - ? this.desktop_settings["Modules"][0].name + const default_page = this.sidebar_configuration + ? this.sidebar_configuration["Modules"][0].name : frappe.boot.allowed_workspaces[0].name; let page = diff --git a/frappe/public/js/frappe/views/inbox/inbox_view.js b/frappe/public/js/frappe/views/inbox/inbox_view.js index f80b07751e..1085e93e6c 100644 --- a/frappe/public/js/frappe/views/inbox/inbox_view.js +++ b/frappe/public/js/frappe/views/inbox/inbox_view.js @@ -69,6 +69,14 @@ frappe.views.InboxView = class InboxView extends frappe.views.ListView { }); } + get_seen_class(doc) { + const seen = + Boolean(doc.seen) || JSON.parse(doc._seen || '[]').includes(frappe.session.user) + ? '' + : 'bold'; + return seen; + } + get is_sent_emails() { const f = this.filter_area.get() .find(filter => filter[1] === 'sent_or_received'); @@ -77,7 +85,7 @@ frappe.views.InboxView = class InboxView extends frappe.views.ListView { render_header() { this.$result.find('.list-row-head').remove(); - this.$result.prepend(this.get_header_html()); + this.$result.prepend(this.get_header_html()); } render() { diff --git a/frappe/public/js/frappe/views/kanban/kanban_view.js b/frappe/public/js/frappe/views/kanban/kanban_view.js index 4b6792330f..4acde8042c 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_view.js +++ b/frappe/public/js/frappe/views/kanban/kanban_view.js @@ -73,6 +73,10 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView { }); } + render_list() { + + } + on_filter_change() { if (JSON.stringify(this.board.filters_array) !== JSON.stringify(this.filter_area.get())) { this.page.set_indicator(__('Not Saved'), 'orange'); diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index f82956adac..53486ea3d6 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -128,10 +128,17 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { () => this.setup_progress_bar(), () => this.setup_page_head(), () => this.refresh_report(), - () => this.add_chart_buttons_to_toolbar(true) + () => this.add_chart_buttons_to_toolbar(true), + () => this.add_card_button_to_toolbar(true), ]); } + add_card_button_to_toolbar() { + this.page.add_inner_button(__("Create Card"), () => { + this.add_card_to_dashboard(); + }); + } + add_chart_buttons_to_toolbar(show) { if (show) { this.page.add_inner_button(__("Set Chart"), () => { @@ -148,29 +155,82 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } } + add_card_to_dashboard() { + let field_options = frappe.report_utils.get_field_options_from_report(this.columns, this.raw_data); + const dashboard_field = frappe.dashboard_utils.get_dashboard_link_field(); + const set_standard = frappe.boot.developer_mode; + + const dialog = new frappe.ui.Dialog({ + title: __('Create Card'), + fields: [ + { + fieldname: 'report_field', + label: __('Field'), + fieldtype: 'Select', + options: field_options.numeric_fields, + }, + { + fieldname: 'cb_1', + fieldtype: 'Column Break' + }, + { + fieldname: 'report_function', + label: __('Function'), + options: ['Sum', 'Average', 'Minimum', 'Maximum'], + fieldtype: 'Select' + }, + { + fieldname: 'sb_1', + label: __('Add to Dashboard'), + fieldtype: 'Section Break' + }, + dashboard_field, + { + fieldname: 'cb_2', + fieldtype: 'Column Break' + }, + { + fieldname: 'label', + label: __('Card Label'), + fieldtype: 'Data', + } + ], + primary_action_label: __('Add'), + primary_action: (values) => { + if (!values.label) { + values.label = `${values.report_function} of ${toTitle(values.report_field)}`; + } + this.create_number_card(values, values.dashboard, values.label, set_standard); + dialog.hide(); + } + }); + + dialog.show(); + + } + add_chart_to_dashboard() { if (this.chart_fields || this.chart_options) { + const dashboard_field = frappe.dashboard_utils.get_dashboard_link_field(); + const set_standard = frappe.boot.developer_mode; + const dialog = new frappe.ui.Dialog({ title: __('Create Chart'), fields: [ - { - fieldname: 'dashboard', - label: 'Choose Dashboard', - fieldtype: 'Link', - options: 'Dashboard', - }, { fieldname: 'dashboard_chart_name', label: 'Chart Name', fieldtype: 'Data', - } + }, + dashboard_field, ], primary_action_label: __('Add'), primary_action: (values) => { this.create_dashboard_chart( this.chart_fields || this.chart_options, values.dashboard, - values.dashboard_chart_name + values.dashboard_chart_name, + set_standard ); dialog.hide(); } @@ -182,7 +242,26 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } } - create_dashboard_chart(chart_args, dashboard_name, chart_name) { + create_number_card(values, dashboard_name, card_name, set_standard) { + let args = { + 'dashboard': dashboard_name || null, + 'type': 'Report', + 'report_name': this.report_name, + 'filters_json': JSON.stringify(this.get_filter_values()), + set_standard: set_standard, + }; + Object.assign(args, values); + + this.add_to_dashboard( + 'frappe.desk.doctype.number_card.number_card.create_report_number_card', + args, + dashboard_name, + card_name, + 'Number Card' + ); + } + + create_dashboard_chart(chart_args, dashboard_name, chart_name, set_standard) { let args = { 'dashboard': dashboard_name || null, 'chart_type': 'Report', @@ -190,7 +269,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { 'type': chart_args.chart_type || frappe.model.unscrub(chart_args.type), 'color': chart_args.color, 'filters_json': JSON.stringify(this.get_filter_values()), - 'custom_options': {} + 'custom_options': {}, + 'set_standard': set_standard, }; for (let key in chart_args) { @@ -211,7 +291,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { 'y_axis': chart_args.y_axis_fields.map(f => { return {'y_field': f.y_field, 'color': f.color}; }), - 'is_custom': 0 + 'use_report_chart': 0 } ); } else { @@ -219,24 +299,34 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { Object.assign(args, { 'chart_name': chart_name, - 'is_custom': 1 + 'use_report_chart': 1 } ); } - frappe.xcall( + this.add_to_dashboard( 'frappe.desk.doctype.dashboard_chart.dashboard_chart.create_report_chart', + args, + dashboard_name, + chart_name, + 'Dashboard Chart' + ); + } + + add_to_dashboard(method, args, dashboard_name, name, doctype) { + frappe.xcall( + method, {args: args} - ).then( () => { + ).then(() => { let message; if (dashboard_name) { let dashboard_route_html = `${dashboard_name}`; - message = __(`New Dashboard Chart ${chart_name} added to Dashboard ` + dashboard_route_html); + message = __(`New {0} {1} added to Dashboard ` + dashboard_route_html, [doctype, name]); } else { - message = __(`New chart ${chart_name} created`); + message = __(`New {0} {1} created`, [doctype, name]); } - frappe.msgprint(message, __('New Chart Created')); + frappe.msgprint(message, __(`New {0} Created`, [doctype])); }); } @@ -518,6 +608,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } this.render_datatable(); this.add_chart_buttons_to_toolbar(true); + this.add_card_button_to_toolbar(); } else { this.data = []; this.toggle_nothing_to_show(true); @@ -700,7 +791,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { open_create_chart_dialog() { const me = this; - let field_options = frappe.report_utils.get_possible_chart_options(this.columns, this.raw_data); + let field_options = frappe.report_utils.get_field_options_from_report(this.columns, this.raw_data); function set_chart_values(values) { values.y_fields = []; @@ -861,8 +952,18 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { if (column.isHeader && !data && this.data) { // totalRow doesn't have a data object // proxy it using the first data object - // this is needed only for currency formatting - data = this.data[0]; + // applied to Float, Currency fields, needed only for currency formatting. + // make first data column have value 'Total' + let index = 1; + if (this.datatable && this.datatable.options.checkboxColumn) index = 2; + + if (column.colIndex === index && !value) { + value = "Total"; + column.fieldtype = "Data"; // avoid type issues for value if Date column + } else if (in_list(["Currency", "Float"], column.fieldtype)) { + // proxy for currency and float + data = this.data[0]; + } } return frappe.format(value, column, {for_print: false, always_show_decimals: true}, data); diff --git a/frappe/public/js/frappe/views/reports/report_utils.js b/frappe/public/js/frappe/views/reports/report_utils.js index 7b1205482f..158dbd653b 100644 --- a/frappe/public/js/frappe/views/reports/report_utils.js +++ b/frappe/public/js/frappe/views/reports/report_utils.js @@ -41,7 +41,7 @@ frappe.report_utils = { } }, - get_possible_chart_options: function(columns, data) { + get_field_options_from_report: function(columns, data) { const rows = data.result.filter(value => Object.keys(value).length); const first_row = Array.isArray(rows[0]) ? rows[0] : columns.map(col => rows[0][col.fieldname]); @@ -138,4 +138,14 @@ frappe.report_utils = { return filter_values; }, + get_result_of_fn(fn, values) { + const get_result = { + 'Minimum': values => values.reduce((min, val) => Math.min(min, val), values[0]), + 'Maximum': values => values.reduce((min, val) => Math.max(min, val), values[0]), + 'Average': values => values.reduce((a, b) => a + b, 0) / values.length, + 'Sum': values => values.reduce((a, b) => a + b, 0) + }; + return get_result[fn](values); + }, + }; diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index 5475c302b7..95a3577698 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -646,11 +646,13 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { set_fields() { if (this.report_name && this.report_doc.json.fields) { - this.fields = this.report_doc.json.fields.slice(); + let fields = this.report_doc.json.fields.slice(); + fields.forEach(f => this._add_field(f[0], f[1])); return; } else if (this.view_user_settings.fields) { // get from user_settings - this.fields = this.view_user_settings.fields; + let fields = this.view_user_settings.fields; + fields.forEach(f => this._add_field(f[0], f[1])); return; } @@ -766,10 +768,12 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { const index = this.fields.findIndex(f => column.field === f[0]); if (index === -1) return; const field = this.fields[index]; - if (field[0] === 'name' && this.group_by === null) { + + if (field[0] === 'name') { this.refresh(); frappe.throw(__('Cannot remove ID field')); } + this.fields.splice(index, 1); this.build_fields(); this.setup_columns(); @@ -848,7 +852,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { columns: 2, options: columns[this.doctype] .filter(df => { - return !df.hidden; + return !df.hidden && df.fieldname !== 'name'; }) .map(df => ({ label: __(df.label), diff --git a/frappe/public/js/frappe/web_form/web_form_list.js b/frappe/public/js/frappe/web_form/web_form_list.js index ec20838dee..21eed52e5e 100644 --- a/frappe/public/js/frappe/web_form/web_form_list.js +++ b/frappe/public/js/frappe/web_form/web_form_list.js @@ -7,16 +7,15 @@ export default class WebFormList { Object.assign(this, opts); frappe.web_form_list = this; this.wrapper = document.getElementById("datatable"); - this.refresh(); this.make_actions(); this.make_filters(); - $('.link-btn').remove() + $('.link-btn').remove(); } refresh() { if (this.table) { Array.from(this.table.tBodies).forEach(tbody => tbody.remove()); - let check = document.getElementById('select-all') + let check = document.getElementById('select-all'); check.checked = false; } this.rows = []; @@ -32,8 +31,8 @@ export default class WebFormList { } make_filters() { - this.filters = {} - this.filter_input = [] + this.filters = {}; + this.filter_input = []; const filter_area = document.getElementById('list-filters'); frappe.call('frappe.website.doctype.web_form.web_form.get_web_form_filters', { @@ -41,9 +40,10 @@ export default class WebFormList { }).then(response => { let fields = response.message; fields.forEach(field => { - let col = document.createElement('div.col-sm-4') - col.classList.add('col', 'col-sm-3') - filter_area.appendChild(col) + let col = document.createElement('div.col-sm-4'); + col.classList.add('col', 'col-sm-3'); + filter_area.appendChild(col); + if (field.default) this.add_filter(field.fieldname, field.default, field.fieldtype); let input = frappe.ui.form.make_control({ df: { @@ -54,27 +54,27 @@ export default class WebFormList { label: __(field.label), onchange: (event) => { $('#more').remove(); - this.add_filter(field.fieldname, input.value, field.fieldtype) + this.add_filter(field.fieldname, input.value, field.fieldtype); + this.refresh(); } }, parent: col, value: field.default, render_input: 1, - }) - this.filter_input.push(input) - }) - }) + }); + this.filter_input.push(input); + }); + this.refresh(); + }); } add_filter(field, value, fieldtype) { if (!value) { - delete this.filters[field] + delete this.filters[field]; + } else { + if (fieldtype === 'Data') value = ['like', value + '%']; + Object.assign(this.filters, Object.fromEntries([[field, value]])); } - else { - if (fieldtype === 'Data') value = ['like', value + '%'] - Object.assign(this.filters, Object.fromEntries([[field, value]])) - } - this.refresh(); } get_list_view_fields() { @@ -106,13 +106,13 @@ export default class WebFormList { } more() { - this.web_list_start += this.page_length + this.web_list_start += this.page_length; this.fetch_data().then((res) => { if (res.message.length === 0) { - frappe.msgprint(__("No more items to display")) + frappe.msgprint(__("No more items to display")); } - this.append_rows(res.message) - }) + this.append_rows(res.message); + }); } @@ -125,7 +125,7 @@ export default class WebFormList { }; }); - if (! this.table) { + if (!this.table) { this.table = document.createElement("table"); this.table.classList.add("table"); this.make_table_head(); diff --git a/frappe/public/js/frappe/widgets/chart_widget.js b/frappe/public/js/frappe/widgets/chart_widget.js index 8a0eca9eaf..ccec5b3ef4 100644 --- a/frappe/public/js/frappe/widgets/chart_widget.js +++ b/frappe/public/js/frappe/widgets/chart_widget.js @@ -255,7 +255,7 @@ export default class ChartWidget extends Widget { } get_report_chart_data(result) { - if (result.chart && this.chart_doc.is_custom) { + if (result.chart && this.chart_doc.use_report_chart) { return result.chart.data; } else { let y_fields = []; @@ -412,10 +412,13 @@ export default class ChartWidget extends Widget { } dialog.show(); - //Set query report object so that it can be used while fetching filter values in the report - frappe.query_report = new frappe.views.QueryReport({'filters': dialog.fields_list}); - frappe.query_reports[this.chart_doc.report_name].onload - && frappe.query_reports[this.chart_doc.report_name].onload(frappe.query_report); + + if (this.chart_doc.chart_type == 'Report') { + //Set query report object so that it can be used while fetching filter values in the report + frappe.query_report = new frappe.views.QueryReport({'filters': dialog.fields_list}); + frappe.query_reports[this.chart_doc.report_name].onload + && frappe.query_reports[this.chart_doc.report_name].onload(frappe.query_report); + } dialog.set_values(this.filters); } @@ -612,9 +615,7 @@ export default class ChartWidget extends Widget { } update_last_synced() { - let last_synced_text = __("Last synced {0}", [ - comment_when(this.chart_doc.last_synced_on) - ]); + let last_synced_text = __("Last synced {0}", [comment_when(this.chart_doc.last_synced_on)]); this.footer.html(last_synced_text); } @@ -636,7 +637,7 @@ export default class ChartWidget extends Widget { set_chart_filters() { let user_saved_filters = this.chart_settings.filters || null; - let chart_saved_filters = JSON.parse(this.chart_doc.filters_json || "null"); + let chart_saved_filters = frappe.dashboard_utils.get_all_filters(this.chart_doc); if (this.chart_doc.chart_type == 'Report') { return frappe.dashboard_utils @@ -653,14 +654,15 @@ export default class ChartWidget extends Widget { } update_default_date_filters(report_filters, chart_filters) { - report_filters.map(f => { - if (['Date', 'DateRange'].includes(f.fieldtype) && f.default) { - if (f.reqd || chart_filters[f.fieldname]) { - chart_filters[f.fieldname] = f.default; + if (report_filters) { + report_filters.map(f => { + if (['Date', 'DateRange'].includes(f.fieldtype) && f.default) { + if (f.reqd || chart_filters[f.fieldname]) { + chart_filters[f.fieldname] = f.default; + } } - } - }); - + }); + } return chart_filters; } diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index 77cb8a59c2..6b38412ebd 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -1,5 +1,5 @@ import Widget from "./base_widget.js"; -import { go_to_list_with_filters, shorten_number } from "./utils"; +import { generate_route, shorten_number } from "./utils"; export default class NumberCardWidget extends Widget { constructor(opts) { @@ -59,20 +59,39 @@ export default class NumberCardWidget extends Widget { } ).then(doc => { this.name = doc.name; - this.card_doc.stats_time_interval = doc.stats_time_interval; - this.card_doc.name = this.name; + this.card_doc = doc; this.widget.attr('data-widget-name', this.name); }); } set_events() { $(this.body).click(() => { - if (this.in_customize_mode) return; - let filters = JSON.parse(this.card_doc.filters_json); - go_to_list_with_filters(this.card_doc.document_type, filters); + if (this.in_customize_mode || this.card_doc.type == 'Custom') return; + this.set_route(); }); } + set_route() { + const is_document_type = this.card_doc.type !== 'Report'; + const name = is_document_type ? this.card_doc.document_type : this.card_doc.report_name; + const route = generate_route({ + name: name, + type: is_document_type ? 'doctype' : 'report', + is_query_report: !is_document_type, + }); + + if (is_document_type) { + const filters = JSON.parse(this.card_doc.filters_json); + frappe.route_options = filters.reduce((acc, filter) => { + return Object.assign(acc, { + [`${filter[0]}.${filter[1]}`]: [filter[2], filter[3]] + }); + }, {}); + } + + frappe.set_route(route); + } + set_doc_args() { this.card_doc = Object.assign({}, { document_type: this.document_type, @@ -84,11 +103,53 @@ export default class NumberCardWidget extends Widget { }); } + get_settings(type) { + this.filters = this.get_filters(); + const settings_map = { + 'Custom': { + method: this.card_doc.method, + args: { + filters: this.filters + }, + get_number: res => this.get_number_for_custom_card(res), + }, + 'Report': { + method: 'frappe.desk.query_report.run', + args: { + report_name: this.card_doc.report_name, + filters: this.filters, + ignore_prepared_report: 1 + }, + get_number: res => this.get_number_for_report_card(res), + }, + 'Document Type': { + method: 'frappe.desk.doctype.number_card.number_card.get_result', + args: { + doc: this.card_doc, + filters: this.filters, + }, + get_number: res => this.get_number_for_doctype_card(res), + } + }; + return settings_map[type]; + } + + get_filters() { + const filters = frappe.dashboard_utils.get_all_filters(this.card_doc); + return filters; + } + render_card() { this.prepare_actions(); this.set_title(); this.set_loading_state(); + if (!this.card_doc.type) { + this.card_doc.type = 'Document Type'; + } + + this.settings = this.get_settings(this.card_doc.type); + frappe.run_serially([ () => this.render_number(), () => this.render_stats(), @@ -102,43 +163,69 @@ export default class NumberCardWidget extends Widget { } get_number() { - return frappe.xcall('frappe.desk.doctype.number_card.number_card.get_result', { - doc: this.card_doc - }).then(res => { - this.number = res; - if (this.card_doc.function !== 'Count') { - return frappe.model.with_doctype(this.card_doc.document_type, () => { - this.get_formatted_number(); - }); - } else { - this.number_html = res; - } + return frappe.xcall(this.settings.method, this.settings.args).then(res => { + return this.settings.get_number(res); }); } - get_formatted_number() { - const based_on_df = - frappe.meta.get_docfield(this.card_doc.document_type, this.card_doc.aggregate_function_based_on); + get_number_for_custom_card(res) { + if (typeof res === 'object') { + this.number = res.value; + this.get_formatted_number(res); + } else { + this.formatted_number = res; + } + } + + get_number_for_doctype_card(res) { + this.number = res; + if (this.card_doc.function !== 'Count') { + return frappe.model.with_doctype(this.card_doc.document_type, () => { + const based_on_df = + frappe.meta.get_docfield(this.card_doc.document_type, this.card_doc.aggregate_function_based_on); + this.get_formatted_number(based_on_df); + }); + } else { + this.formatted_number = res; + } + } + + get_number_for_report_card(res) { + const field = this.card_doc.report_field; + const vals = res.result.reduce((acc, col) => { + col[field] && acc.push(col[field]); + return acc; + }, []); + const col = res.columns.find(col => col.fieldname == field); + this.number = frappe.report_utils.get_result_of_fn(this.card_doc.report_function, vals); + this.get_formatted_number(col); + } + + get_formatted_number(df) { const default_country = frappe.sys_defaults.country; const shortened_number = shorten_number(this.number, default_country); let number_parts = shortened_number.split(' '); const symbol = number_parts[1] || ''; - const formatted_number = $(frappe.format(number_parts[0], based_on_df)).text(); + const formatted_number = $(frappe.format(number_parts[0], df)).text(); - this.number_html = formatted_number + ' ' + symbol; + this.formatted_number = formatted_number + ' ' + symbol; } render_number() { return this.get_number().then(() => { $(this.body).html(`
    -
    ${this.number_html}
    +
    ${this.formatted_number}
    `); }); } render_stats() { - let caret_html =''; + if (this.card_doc.type !== 'Document Type') { + return; + } + + let caret_html = ''; let color_class = ''; return this.get_percentage_stats().then(() => { @@ -177,6 +264,7 @@ export default class NumberCardWidget extends Widget { get_percentage_stats() { return frappe.xcall('frappe.desk.doctype.number_card.number_card.get_percentage_difference', { doc: this.card_doc, + filters: this.filters, result: this.number }).then(res => { if (res !== undefined) { diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js index 4c91994a6f..8c1d2cbb5b 100644 --- a/frappe/public/js/frappe/widgets/onboarding_widget.js +++ b/frappe/public/js/frappe/widgets/onboarding_widget.js @@ -136,7 +136,7 @@ export default class OnboardingWidget extends Widget { if (step.is_single) { route = `Form/${step.reference_document}`; } else { - route = `Form/${step.reference_document}/__('New')+ ' ' + ${step.reference_document}`; + route = `Form/${step.reference_document}/${__('New')} ${__(step.reference_document)} 1`; } let current_route = frappe.get_route(); @@ -262,7 +262,7 @@ export default class OnboardingWidget extends Widget { frappe.route_hooks.after_save = callback; } - frappe.set_route(`Form/${step.reference_document}/__('New')+ ' ' + ${step.reference_document}`); + frappe.set_route(`Form/${step.reference_document}/${__('New')} ${__(step.reference_document)} 1`); } show_quick_entry(step) { diff --git a/frappe/public/js/frappe/widgets/utils.js b/frappe/public/js/frappe/widgets/utils.js index dff4db807e..22b3167977 100644 --- a/frappe/public/js/frappe/widgets/utils.js +++ b/frappe/public/js/frappe/widgets/utils.js @@ -117,16 +117,6 @@ const build_summary_item = (summary) => {
    `); }; -function go_to_list_with_filters(doctype, filters) { - const route = `List/${doctype}/List`; - frappe.set_route(route).then(()=> { - let list_view = frappe.views.list_view[route]; - let filter_area = list_view.filter_area; - filter_area.clear(); - filter_area.filter_list.add_filters_to_filter_group(filters); - }); -} - function shorten_number(number, country) { country = (country == 'India') ? country : ''; const number_system = get_number_system(country); @@ -167,4 +157,4 @@ function get_number_system(country) { return number_system_map[country]; } -export { generate_route, generate_grid, build_summary_item, go_to_list_with_filters, shorten_number }; \ No newline at end of file +export { generate_route, generate_grid, build_summary_item, shorten_number }; \ No newline at end of file diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js index 054159116f..2ca26d59e1 100644 --- a/frappe/public/js/frappe/widgets/widget_dialog.js +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -174,18 +174,12 @@ class ShortcutDialog extends WidgetDialog { onchange: () => { if (this.dialog.get_value("type") == "DocType") { let doctype = this.dialog.get_value("link_to"); - - doctype && - frappe.db - .get_value("DocType", doctype, "issingle") - .then((res) => { - if (res.message && res.message.issingle) { - this.hide_filters(); - } else { - this.setup_filter(doctype); - this.show_filters(); - } - }); + if (doctype && frappe.boot.single_types.includes(doctype)) { + this.hide_filters(); + } else if (doctype) { + this.setup_filter(doctype); + this.show_filters(); + } } else { this.hide_filters(); } diff --git a/frappe/public/less/dashboard_view.less b/frappe/public/less/dashboard_view.less index ab78fa5b2a..17fa52cc0f 100644 --- a/frappe/public/less/dashboard_view.less +++ b/frappe/public/less/dashboard_view.less @@ -33,13 +33,6 @@ line-height: 1.5em; vertical-align: text-bottom; } - - .restricted-button { - cursor: default; - position: relative; - right: 5px; - top: -3px; - } } .customize-dashboard { diff --git a/frappe/public/less/desk.less b/frappe/public/less/desk.less index b0fb60b6a3..36ceed9bfa 100644 --- a/frappe/public/less/desk.less +++ b/frappe/public/less/desk.less @@ -787,13 +787,14 @@ li.user-progress { margin-right: 5px; margin-left: 0px; position: relative; - height: 12px; } +@checkbox-height: 14px; + // custom font awesome checkbox input[type="checkbox"] { position: relative; - visibility: hidden; + height: 0px !important; &:before { position: absolute; @@ -804,9 +805,10 @@ input[type="checkbox"] { font-weight: normal; font-variant: normal; text-transform: none; - line-height: 14px; - display: inline-block; - font-size: 14px; + line-height: @checkbox-height; + margin-top: -10px; + display: inline-block !important; + font-size: @checkbox-height; color: @text-extra-muted; .transition(150ms color); left: 0px; @@ -818,10 +820,17 @@ input[type="checkbox"] { &:checked:before { content: '\f14a'; - font-size: 13px; + font-size: @checkbox-height - 1px; color: @checkbox-color; } + &:checked:focus:before { + box-shadow: inset 0 0 1px 2px @text-muted; + padding: 0px 1px; + border-radius: 3px; + height: @checkbox-height - 1px; + } + &:focus { outline: none; } @@ -836,28 +845,11 @@ input[type="checkbox"] { height: 100%; } -// mozilla doesn't support +// Firefox doesn't support // pseudo elements on checkbox -@-moz-document url-prefix() { +@supports (-moz-appearance: none) or (-ms-ime-align:auto) { input[type="checkbox"] { - visibility: visible; - left: 0; - } -} - -@supports (-moz-appearance: none) { - input[type="checkbox"] { - visibility: visible; - left: 0; - } -} - -// edge doesn't support pseudo elements on checkbox -//Microsoft Edge Browser 12+ (All) -@supports (-ms-ime-align:auto) { - input[type="checkbox"] { - visibility: visible; - left: 0; + height: @checkbox-height !important } } diff --git a/frappe/public/less/desktop.less b/frappe/public/less/desktop.less index 3e3b59ddf8..a738679cc8 100644 --- a/frappe/public/less/desktop.less +++ b/frappe/public/less/desktop.less @@ -143,6 +143,13 @@ } } +.frappe-rtl { + .desk-body { + padding-left: 0px; + padding-right: calc(20rem + 15px); + } +} + .widget-group { margin-bottom: 25px; // -webkit-animation-name: slideInUp; diff --git a/frappe/public/less/email.less b/frappe/public/less/email.less index b6d9540586..bf0507138b 100644 --- a/frappe/public/less/email.less +++ b/frappe/public/less/email.less @@ -10,6 +10,13 @@ p { margin: 1em 0 !important; } +.ql-editor { + white-space: normal; + p { + margin: 0 !important; + } +} + hr { border-top: 1px solid @border-color; } @@ -210,4 +217,4 @@ hr { .report-title { margin-bottom: 20px; } -/* csslint ignore:end */ +/* csslint ignore:end */ \ No newline at end of file diff --git a/frappe/public/less/form.less b/frappe/public/less/form.less index cd391c1f10..8d01cd6dd7 100644 --- a/frappe/public/less/form.less +++ b/frappe/public/less/form.less @@ -349,6 +349,9 @@ h6.uppercase, .h6.uppercase { .form-section { padding: 15px 7px; } + .hide-border { + padding-top: 0; + } } .help ol { @@ -573,7 +576,13 @@ h6.uppercase, .h6.uppercase { margin-left: 5px; } - .media-body:after, .media-body:before { + .media-body { + .left-arrow; + } +} + +.left-arrow { + &::after, &::before { right: 100%; top: 15px; border: solid transparent; @@ -584,13 +593,13 @@ h6.uppercase, .h6.uppercase { pointer-events: none; } - .media-body:after { + &::after { border-color: rgba(136, 183, 213, 0); border-right-color: #fafbfc; border-width: 6px; margin-top: -6px; } - .media-body:before { + &::before { border-color: rgba(194, 225, 245, 0); border-right-color: @border-color; border-width: 7px; @@ -638,6 +647,18 @@ h6.uppercase, .h6.uppercase { top: 5px; } +.timeline-item.user-content.show-indicator { + position: relative; + .media-body { + margin-left: 50px; + } + &::before { + .timeline-indicator(); + left: 13px; + top: 13px; + } +} + .timeline-item.notification-content::before { .timeline-indicator(); } diff --git a/frappe/public/less/form_grid.less b/frappe/public/less/form_grid.less index 5cb04a252c..d9e7d8bceb 100644 --- a/frappe/public/less/form_grid.less +++ b/frappe/public/less/form_grid.less @@ -98,6 +98,10 @@ text-align: right; } +.grid-row .grid-row-check { + margin-top: 12px; +} + .grid-row > .row { .col:last-child { margin-right: -10px; diff --git a/frappe/public/less/list.less b/frappe/public/less/list.less index 4e066f86e4..4c7d04406d 100644 --- a/frappe/public/less/list.less +++ b/frappe/public/less/list.less @@ -197,7 +197,7 @@ body.no-list-sidebar { } input.list-check-all, input.list-row-checkbox { - margin-top: 0px; + display: inline-block; } .filterable { diff --git a/frappe/public/less/quill.less b/frappe/public/less/quill.less index a72602697a..514d8993cd 100644 --- a/frappe/public/less/quill.less +++ b/frappe/public/less/quill.less @@ -9,10 +9,6 @@ font-family: inherit; } -.ql-editor { - white-space: normal; -} - .ql-editor { font-family: @font-stack; line-height: 1.6; @@ -82,7 +78,7 @@ } } -.ql-editor .mention { +.ql-editor:not(.read-mode) .mention { height: auto; width: auto; border-radius: 10px; @@ -137,3 +133,7 @@ margin-top: 0px; margin-bottom: 0px; } + +.ql-editor.read-mode { + padding: 0; +} \ No newline at end of file diff --git a/frappe/public/scss/portal.scss b/frappe/public/scss/portal.scss new file mode 100644 index 0000000000..33371d5693 --- /dev/null +++ b/frappe/public/scss/portal.scss @@ -0,0 +1,7 @@ +.portal-row { + padding: 1rem 0; + + a { + color: $body-color; + } +} \ No newline at end of file diff --git a/frappe/public/scss/sidebar.scss b/frappe/public/scss/sidebar.scss index d3442c2344..b13eaf2a74 100644 --- a/frappe/public/scss/sidebar.scss +++ b/frappe/public/scss/sidebar.scss @@ -20,6 +20,11 @@ } } +// Remove top margin from frist child +.sidebar-item:first-child a { + margin-top: 0rem; +} + .sidebar-item a.active { color: $primary; background-color: $primary-light; diff --git a/frappe/public/scss/website.scss b/frappe/public/scss/website.scss index e03c502784..3c11d23252 100644 --- a/frappe/public/scss/website.scss +++ b/frappe/public/scss/website.scss @@ -1,3 +1,4 @@ +@import '~quill/dist/quill.core'; @import 'variables'; @import 'frappe/public/css/font-awesome'; @import '~bootstrap/scss/bootstrap'; @@ -8,8 +9,23 @@ @import 'blog'; @import 'markdown'; @import 'sidebar'; +@import 'portal'; @import 'doc'; +.ql-editor.read-mode { + padding: 0; + line-height: 1.6; + + h1, + h2, + h3, + h4, + h5 { + margin-top: 0.5em; + margin-bottom: 0.25em; + } +} + .container { padding-left: 1.25rem; padding-right: 1.25rem; @@ -110,8 +126,13 @@ color: $light; } +.page-content-wrapper { + margin: 2rem 0; +} + .breadcrumb-container { margin-top: 1rem; + padding-top: 0.25rem; } .breadcrumb { @@ -326,4 +347,10 @@ h5.modal-title { left: 0; width: 100%; height: 100%; -} \ No newline at end of file +} + +.ellipsis { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} diff --git a/frappe/sessions.py b/frappe/sessions.py index d317d6caf3..7a018bb0aa 100644 --- a/frappe/sessions.py +++ b/frappe/sessions.py @@ -172,13 +172,6 @@ def generate_csrf_token(): frappe.local.session.data.csrf_token = frappe.generate_hash() frappe.local.session_obj.update(force=True) - # send sid and csrf token to the user - # handles the case when a user logs in again from another tab - # and it leads to invalid request in the current tab - frappe.publish_realtime(event="csrf_generated", - message={"sid": frappe.local.session.sid, "csrf_token": frappe.local.session.data.csrf_token}, - user=frappe.session.user, after_commit=True) - class Session: def __init__(self, user, resume=False, full_name=None, user_type=None): self.sid = cstr(frappe.form_dict.get('sid') or diff --git a/frappe/templates/base.html b/frappe/templates/base.html index 0b82b3dac2..8c843a44a4 100644 --- a/frappe/templates/base.html +++ b/frappe/templates/base.html @@ -42,7 +42,11 @@ {{ head_include or "" }} {% endblock -%} - {%- block style %}{%- endblock -%} + {%- block style %} + {% if colocated_css -%} + + {%- endif %} + {%- endblock -%} {%- endfor -%} - {%- block script %}{%- endblock %} + {%- block script %} + {% if colocated_js -%} + + {%- endif %} + {%- endblock %} {%- block body_include %}{{ body_include or "" }}{% endblock -%} diff --git a/frappe/templates/includes/comments/comments.html b/frappe/templates/includes/comments/comments.html index ef53effe53..ffd09523af 100644 --- a/frappe/templates/includes/comments/comments.html +++ b/frappe/templates/includes/comments/comments.html @@ -9,10 +9,10 @@ {% endif %}
    - {% for comment in comment_list %} -
    - {% include "templates/includes/comments/comment.html" %} -
    + {% for comment in comment_list %} +
    + {% include "templates/includes/comments/comment.html" %} +
    {% endfor %}
    @@ -25,26 +25,22 @@
    {{ _("Add Comment") }} -