diff --git a/frappe/custom/doctype/client_script/client_script.js b/frappe/custom/doctype/client_script/client_script.js index 21e7334b82..27d11af4d1 100644 --- a/frappe/custom/doctype/client_script/client_script.js +++ b/frappe/custom/doctype/client_script/client_script.js @@ -8,40 +8,42 @@ frappe.ui.form.on('Client Script', { () => frappe.set_route('List', frm.doc.dt, 'List')); } - frm.add_custom_button(__('Add script for Child Table'), () => { - frappe.model.with_doctype(frm.doc.dt, () => { - const child_tables = frappe.meta.get_docfields(frm.doc.dt, null, { - fieldtype: 'Table' - }).map(df => df.options); + if (frm.doc.view == 'Form') { + frm.add_custom_button(__('Add script for Child Table'), () => { + frappe.model.with_doctype(frm.doc.dt, () => { + const child_tables = frappe.meta.get_docfields(frm.doc.dt, null, { + fieldtype: 'Table' + }).map(df => df.options); - const d = new frappe.ui.Dialog({ - title: __('Select Child Table'), - fields: [ - { - label: __('Select Child Table'), - fieldtype: 'Link', - fieldname: 'cdt', - options: 'DocType', - get_query: () => { - return { - filters: { - istable: 1, - name: ['in', child_tables] - } - }; + const d = new frappe.ui.Dialog({ + title: __('Select Child Table'), + fields: [ + { + label: __('Select Child Table'), + fieldtype: 'Link', + fieldname: 'cdt', + options: 'DocType', + get_query: () => { + return { + filters: { + istable: 1, + name: ['in', child_tables] + } + }; + } } + ], + primary_action: ({ cdt }) => { + cdt = d.get_field('cdt').value; + frm.events.add_script_for_doctype(frm, cdt); + d.hide(); } - ], - primary_action: ({ cdt }) => { - cdt = d.get_field('cdt').value; - frm.events.add_script_for_doctype(frm, cdt); - d.hide(); - } - }); + }); - d.show(); + d.show(); + }); }); - }); + } frm.set_query('dt', { filters: { @@ -51,6 +53,8 @@ frappe.ui.form.on('Client Script', { }, dt(frm) { + frm.toggle_display('view', !frappe.boot.single_types.includes(frm.doc.dt)); + if (!frm.doc.script) { frm.events.add_script_for_doctype(frm, frm.doc.dt); } @@ -61,7 +65,18 @@ frappe.ui.form.on('Client Script', { } }, + view(frm) { + let has_form_boilerplate = frm.doc.script.includes('frappe.ui.form.on') + if (frm.doc.view === 'List' && has_form_boilerplate) { + frm.set_value('script', ''); + } + if (frm.doc.view === 'Form' && !has_form_boilerplate) { + frm.trigger('dt'); + } + }, + add_script_for_doctype(frm, doctype) { + if (!doctype) return; let boilerplate = ` frappe.ui.form.on('${doctype}', { refresh(frm) { diff --git a/frappe/custom/doctype/client_script/client_script.json b/frappe/custom/doctype/client_script/client_script.json index 57e6c68094..db02d8d4bc 100644 --- a/frappe/custom/doctype/client_script/client_script.json +++ b/frappe/custom/doctype/client_script/client_script.json @@ -8,6 +8,7 @@ "engine": "InnoDB", "field_order": [ "dt", + "view", "enabled", "script", "sample" @@ -22,7 +23,8 @@ "oldfieldname": "dt", "oldfieldtype": "Link", "options": "DocType", - "reqd": 1 + "reqd": 1, + "set_only_once": 1 }, { "fieldname": "script", @@ -43,13 +45,21 @@ "fieldname": "enabled", "fieldtype": "Check", "label": "Enabled" + }, + { + "default": "Form", + "fieldname": "view", + "fieldtype": "Select", + "label": "Apply To", + "options": "List\nForm", + "set_only_once": 1 } ], "icon": "fa fa-glass", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2021-02-04 13:57:56.509437", + "modified": "2021-03-16 20:33:51.400191", "modified_by": "Administrator", "module": "Custom", "name": "Client Script", diff --git a/frappe/custom/doctype/client_script/client_script.py b/frappe/custom/doctype/client_script/client_script.py index e252e2a750..049f979263 100644 --- a/frappe/custom/doctype/client_script/client_script.py +++ b/frappe/custom/doctype/client_script/client_script.py @@ -3,15 +3,29 @@ from __future__ import unicode_literals import frappe +from frappe import _ from frappe.model.document import Document + class ClientScript(Document): def autoname(self): - self.name = self.dt + self.name = f"{self.dt}-{self.view}" + + def validate(self): + if not self.is_new(): + return + + exists = frappe.db.exists( + "Client Script", {"dt": self.dt, "view": self.view} + ) + if exists: + frappe.throw( + _("Client Script for {0} {1} already exists").format(frappe.bold(self.dt), self.view), + frappe.DuplicateEntryError, + ) def on_update(self): frappe.clear_cache(doctype=self.dt) def on_trash(self): frappe.clear_cache(doctype=self.dt) - diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index b19f6cf9f0..48b34e6cd9 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -171,7 +171,6 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): doctype = chart.document_type datefield = chart.based_on - aggregate_function = get_aggregate_function(chart.chart_type) value_field = chart.value_based_on or '1' from_date = from_date.strftime('%Y-%m-%d') to_date = to_date @@ -183,7 +182,8 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): doctype, fields = [ '{} as _unit'.format(datefield), - '{aggregate_function}({value_field})'.format(aggregate_function=aggregate_function, value_field=value_field), + 'SUM({})'.format(value_field), + 'COUNT(*)' ], filters = filters, group_by = '_unit', @@ -192,7 +192,7 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): ignore_ifnull = True ) - result = get_result(data, timegrain, from_date, to_date) + result = get_result(data, timegrain, from_date, to_date, chart.chart_type) chart_config = { "labels": [get_period(r[0], timegrain) for r in result], @@ -288,15 +288,21 @@ def get_aggregate_function(chart_type): }[chart_type] -def get_result(data, timegrain, from_date, to_date): +def get_result(data, timegrain, from_date, to_date, chart_type): dates = get_dates_from_timegrain(from_date, to_date, timegrain) result = [[date, 0] for date in dates] data_index = 0 if data: for i, d in enumerate(result): + count = 0 while data_index < len(data) and getdate(data[data_index][0]) <= d[0]: d[1] += data[data_index][1] + count += data[data_index][2] data_index += 1 + if chart_type == 'Average' and not count == 0: + d[1] = d[1]/count + if chart_type == 'Count': + d[1] = count return result diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py index 3c37ad4a09..72ab18385d 100644 --- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py @@ -212,19 +212,52 @@ class TestDashboardChart(unittest.TestCase): frappe.db.rollback() -def insert_test_records(): - 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 test_avg_dashboard_chart(self): + insert_test_records() -def create_new_communication(date, rating): + if frappe.db.exists('Dashboard Chart', 'Test Average Dashboard Chart'): + frappe.delete_doc('Dashboard Chart', 'Test Average Dashboard Chart') + + frappe.get_doc(dict( + doctype = 'Dashboard Chart', + chart_name = 'Test Average Dashboard Chart', + chart_type = 'Average', + document_type = 'Communication', + based_on = 'communication_date', + value_based_on = 'rating', + timespan = 'Select Date Range', + time_interval = 'Weekly', + from_date = datetime(2018, 12, 30), + to_date = datetime(2019, 1, 15), + filters_json = '[]', + timeseries = 1 + )).insert() + + result = get(chart_name='Test Average Dashboard Chart', refresh = 1) + + self.assertEqual(result.get('datasets')[0].get('values'), [50.0, 150.0, 266.6666666666667, 0.0]) + self.assertEqual( + result.get('labels'), + ['30-12-18', '06-01-19', '13-01-19', '20-01-19'] + ) + + frappe.db.rollback() + +def insert_test_records(): + create_new_communication('Communication 1', datetime(2018, 12, 30), 50) + create_new_communication('Communication 2', datetime(2019, 1, 4), 100) + create_new_communication('Communication 3', datetime(2019, 1, 6), 200) + create_new_communication('Communication 4', datetime(2019, 1, 7), 400) + create_new_communication('Communication 5', datetime(2019, 1, 8), 300) + create_new_communication('Communication 6', datetime(2019, 1, 10), 100) + +def create_new_communication(subject, date, rating): communication = { 'doctype': 'Communication', - 'subject': 'Test Communication', + 'subject': subject, 'rating': rating, 'communication_date': date } - frappe.get_doc(communication).insert() + comm = frappe.get_doc(communication) + if not frappe.db.exists("Communication", {'subject' : comm.subject}): + comm.insert() diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py index 733ee1774c..a62e2837d5 100644 --- a/frappe/desk/form/linked_with.py +++ b/frappe/desk/form/linked_with.py @@ -79,28 +79,30 @@ def get_submitted_linked_docs(doctype, name, docs=None, visited=None): @frappe.whitelist() def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=[]): """ - Cancel all linked doctype + Cancel all linked doctype, optionally ignore doctypes specified in a list. Arguments: - docs (str) - It contains all list of dictionaries of a linked documents. + docs (json str) - It contains list of dictionaries of a linked documents. + ignore_doctypes_on_cancel_all (list) - List of doctypes to ignore while cancelling. """ docs = json.loads(docs) if isinstance(ignore_doctypes_on_cancel_all, string_types): ignore_doctypes_on_cancel_all = json.loads(ignore_doctypes_on_cancel_all) for i, doc in enumerate(docs, 1): - if validate_linked_doc(doc, ignore_doctypes_on_cancel_all) is True: - frappe.publish_progress(percent=i * 100 / ((len(docs) - len(ignore_doctypes_on_cancel_all))), title=_("Cancelling documents")) + if validate_linked_doc(doc, ignore_doctypes_on_cancel_all): linked_doc = frappe.get_doc(doc.get("doctype"), doc.get("name")) linked_doc.cancel() + frappe.publish_progress(percent=i/len(docs) * 100, title=_("Cancelling documents")) def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=[]): """ Validate a document to be submitted and non-exempted from auto-cancel. - Args: - docs (dict): The document to check for submitted and non-exempt from auto-cancel + Arguments: + docinfo (dict): The document to check for submitted and non-exempt from auto-cancel + ignore_doctypes_on_cancel_all (list) - List of doctypes to ignore while cancelling. Returns: bool: True if linked document passes all validations, else False diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py index c63da93a33..e637f4969a 100644 --- a/frappe/desk/form/meta.py +++ b/frappe/desk/form/meta.py @@ -63,7 +63,7 @@ class FormMeta(Meta): "__linked_with", "__messages", "__print_formats", "__workflow_docs", "__form_grid_templates", "__listview_template", "__tree_js", "__dashboard", "__kanban_column_fields", '__templates', - '__custom_js'): + '__custom_js', '__custom_list_js'): d[k] = self.get(k) # d['fields'] = d.get('fields', []) @@ -130,9 +130,23 @@ class FormMeta(Meta): def add_custom_script(self): """embed all require files""" # custom script - custom = frappe.db.get_value("Client Script", {"dt": self.name, "enabled": 1}, "script") or "" + client_scripts = frappe.db.get_all("Client Script", + filters={"dt": self.name, "enabled": 1}, + fields=["script", "view"], + order_by="creation asc" + ) or "" - self.set("__custom_js", custom) + list_script = '' + form_script = '' + for script in client_scripts: + if script.view == 'List': + list_script += script.script + + if script.view == 'Form': + form_script += script.script + + self.set("__custom_js", form_script) + self.set("__custom_list_js", list_script) def add_search_fields(self): """add search fields found in the doctypes indicated by link fields' options""" diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 3008cf0e61..22d47d1120 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -164,10 +164,14 @@ def get_script(report_name): module = report.module or frappe.db.get_value( "DocType", report.ref_doctype, "module" ) - module_path = get_module_path(module) - report_folder = os.path.join(module_path, "report", scrub(report.name)) - script_path = os.path.join(report_folder, scrub(report.name) + ".js") - print_path = os.path.join(report_folder, scrub(report.name) + ".html") + + is_custom_module = frappe.get_cached_value("Module Def", module, "custom") + + # custom modules are virtual modules those exists in DB but not in disk. + module_path = '' if is_custom_module else get_module_path(module) + report_folder = module_path and os.path.join(module_path, "report", scrub(report.name)) + script_path = report_folder and os.path.join(report_folder, scrub(report.name) + ".js") + print_path = report_folder and os.path.join(report_folder, scrub(report.name) + ".html") script = None if os.path.exists(script_path): diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 396cd983fb..6e6635caf6 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -707,25 +707,18 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { const field_html = () => { let html; let _value; - // listview_setting formatter - if ( - this.settings.formatters && - this.settings.formatters[fieldname] - ) { - _value = this.settings.formatters[fieldname](value, df, doc); + let strip_html_required = + df.fieldtype == "Text Editor" || + (df.fetch_from && + ["Text", "Small Text"].includes(df.fieldtype)); + + if (strip_html_required) { + _value = strip_html(value); } else { - let strip_html_required = - df.fieldtype == "Text Editor" || - (df.fetch_from && - ["Text", "Small Text"].includes(df.fieldtype)); - if (strip_html_required) { - _value = strip_html(value); - } else { - _value = - typeof value === "string" - ? frappe.utils.escape_html(value) - : value; - } + _value = + typeof value === "string" + ? frappe.utils.escape_html(value) + : value; } if (df.fieldtype === "Image") { @@ -781,7 +774,15 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { Subject: this.get_subject_html(doc), Field: field_html(), }; - const column_html = html_map[col.type]; + let column_html = html_map[col.type]; + + // listview_setting formatter + if ( + this.settings.formatters && + this.settings.formatters[fieldname] + ) { + column_html = this.settings.formatters[fieldname](value, df, doc); + } return `
@@ -912,7 +913,14 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { get_subject_html(doc) { let subject_field = this.columns[0].df; - let value = doc[subject_field.fieldname] || doc.name; + let value = doc[subject_field.fieldname]; + if (this.settings.formatters && this.settings.formatters[subject_field.fieldname]) { + let formatter = this.settings.formatters[subject_field.fieldname]; + value = formatter(value, subject_field, doc); + } + if (!value) { + value = doc.name; + } let subject = strip_html(value.toString()); let escaped_subject = frappe.utils.escape_html(subject); diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 9ec7b0e931..22a5180a2b 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -181,6 +181,9 @@ $.extend(frappe.model, { if(meta.__list_js) { eval(meta.__list_js); } + if(meta.__custom_list_js) { + eval(meta.__custom_list_js); + } if(meta.__calendar_js) { eval(meta.__calendar_js); } diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index a5f078fc7d..ec944d5bcf 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -775,8 +775,8 @@ frappe.views.CommunicationComposer = Class.extend({ let communication_date = last_email.communication_date || last_email.creation; content = ` -

${reply} +

${frappe.separator_element || ''}

${__("On {0}, {1} wrote:", [frappe.datetime.global_date_format(communication_date) , last_email.sender])}

@@ -784,7 +784,7 @@ frappe.views.CommunicationComposer = Class.extend({
`; } else { - content = "

" + reply; + content = reply; } fields.content.set_value(content); }, diff --git a/frappe/test_runner.py b/frappe/test_runner.py index b66a96595d..0e81d69593 100644 --- a/frappe/test_runner.py +++ b/frappe/test_runner.py @@ -179,6 +179,8 @@ def run_tests_for_module(module, verbose=False, tests=(), profile=False, junit_x return _run_unittest(module, verbose=verbose, tests=tests, profile=profile, junit_xml_output=junit_xml_output) def _run_unittest(modules, verbose=False, tests=(), profile=False, junit_xml_output=False): + frappe.db.begin() + test_suite = unittest.TestSuite() if not isinstance(modules, (list, tuple)):