From 3aa3832bd3b5e0fb339c05615cb3eeb2de64ff58 Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 4 Jun 2020 16:01:49 +0530 Subject: [PATCH] feat: sync dashboards from json files --- frappe/desk/doctype/dashboard/dashboard.js | 14 ++ frappe/desk/doctype/dashboard/dashboard.json | 27 +++- frappe/desk/doctype/dashboard/dashboard.py | 28 ++++ .../dashboard_chart/dashboard_chart.js | 13 +- .../dashboard_chart/dashboard_chart.json | 15 +- .../dashboard_chart/dashboard_chart.py | 6 +- .../desk/doctype/number_card/number_card.js | 3 + .../desk/doctype/number_card/number_card.json | 10 +- .../desk/doctype/number_card/number_card.py | 3 + frappe/modules/export_file.py | 12 +- frappe/public/js/frappe/form/form.js | 9 ++ frappe/utils/dashboard.py | 128 +++++++++++++----- 12 files changed, 217 insertions(+), 51 deletions(-) diff --git a/frappe/desk/doctype/dashboard/dashboard.js b/frappe/desk/doctype/dashboard/dashboard.js index 609e943995..cf06d91c4d 100644 --- a/frappe/desk/doctype/dashboard/dashboard.js +++ b/frappe/desk/doctype/dashboard/dashboard.js @@ -5,6 +5,20 @@ 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.disable_form(); + } else { + frm.add_custom_button(__("Export"), () => { + frappe.call({ + method: 'frappe.desk.doctype.dashboard.dashboard.export_dashboard', + freeze: true, + args: { + doc: frm.doc + } + }); + }); + } + frm.set_query("chart", "charts", function() { return { filters: { diff --git a/frappe/desk/doctype/dashboard/dashboard.json b/frappe/desk/doctype/dashboard/dashboard.json index c0e2bddcf8..6093ac037a 100644 --- a/frappe/desk/doctype/dashboard/dashboard.json +++ b/frappe/desk/doctype/dashboard/dashboard.json @@ -8,6 +8,8 @@ "field_order": [ "dashboard_name", "is_default", + "is_standard", + "module", "charts", "chart_options", "cards" @@ -35,21 +37,34 @@ "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": 1 + }, + { + "fieldname": "module", + "fieldtype": "Link", + "label": "Module", + "options": "Module Def" } ], "links": [], - "modified": "2020-04-29 13:26:37.362482", + "modified": "2020-05-26 18:31:10.062311", "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..4fc0c89544 100644 --- a/frappe/desk/doctype/dashboard/dashboard.py +++ b/frappe/desk/doctype/dashboard/dashboard.py @@ -4,9 +4,11 @@ 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 +from frappe.utils.dashboard import create_filters_file_after_export class Dashboard(Document): def on_update(self): @@ -16,6 +18,8 @@ class Dashboard(Document): tabDashboard set is_default = 0 where name != %s''', self.name) def validate(self): + if not frappe.conf.developer_mode and self.is_standard: + frappe.throw('Cannot edit Standard Dashboards') self.validate_custom_options() def validate_custom_options(self): @@ -25,6 +29,30 @@ class Dashboard(Document): except ValueError as error: frappe.throw(_("Invalid json added in the custom options: {0}").format(error)) +@frappe.whitelist() +def export_dashboard(doc): + doc = frappe._dict(frappe.parse_json(doc)) + card_count = 0 + chart_count = 0 + + if not doc.module: + frappe.msgprint(_('Please set Module')) + + if frappe.conf.developer_mode and doc.module: + export_to_files(record_list=[['Dashboard', doc.name, doc.module + ' Dashboard']], record_module=doc.module,) + record_list = [] + for chart in doc.charts: + record_list.append(['Dashboard Chart', chart.get('chart'), 'Dashboard Charts']) + chart_count+=1 + for card in doc.cards: + record_list.append(['Number Card', card.get('card'), 'Number Cards']) + card_count+=1 + + export_to_files(record_list=record_list, record_module=doc.module) + frappe.msgprint(_('Successfully exported {chart_count} Charts and {card_count} Cards').format(chart_count=chart_count, card_count=card_count)) + + create_filters_file_after_export(module_name=doc.module.lower(), dashboard_name=doc.name) + @frappe.whitelist() def get_permitted_charts(dashboard_name): permitted_charts = [] diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js index a10d3d96f2..a24526957d 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js @@ -12,6 +12,11 @@ frappe.ui.form.on('Dashboard Chart', { refresh: function(frm) { frm.chart_filters = null; + + if (!frappe.boot.developer_mode && frm.doc.is_standard) { + frm.disable_form(); + } + frm.add_custom_button('Add Chart to Dashboard', () => { const d = new frappe.ui.Dialog({ title: __('Add to Dashboard'), @@ -240,11 +245,11 @@ 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'); }); }, diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json index 4bab76337f..1e67b56574 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json @@ -22,6 +22,7 @@ "aggregate_function_based_on", "number_of_groups", "column_break_6", + "is_standard", "is_public", "heatmap_year", "timespan", @@ -33,9 +34,9 @@ "filters_section", "filters_json", "chart_options_section", - "color", - "column_break_2", "custom_options", + "column_break_2", + "color", "section_break_10", "last_synced_on" ], @@ -235,10 +236,18 @@ "fieldname": "heatmap_year", "fieldtype": "Select", "label": "Year" + }, + { + "default": "0", + "fieldname": "is_standard", + "fieldtype": "Check", + "label": "Is Standard", + "show_days": 1, + "show_seconds": 1 } ], "links": [], - "modified": "2020-05-16 15:03:02.455395", + "modified": "2020-06-04 15:59:52.046492", "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 4ad6943e0b..a50d51955b 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -80,7 +80,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]) @@ -349,6 +351,8 @@ class DashboardChart(Document): frappe.cache().delete_key('chart-data:{}'.format(self.name)) 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/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js index 184fe5e6cb..56f1231760 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -3,6 +3,9 @@ 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.trigger('set_options'); frm.trigger('render_filters_table'); diff --git a/frappe/desk/doctype/number_card/number_card.json b/frappe/desk/doctype/number_card/number_card.json index ec6a1e9190..f911737e8e 100644 --- a/frappe/desk/doctype/number_card/number_card.json +++ b/frappe/desk/doctype/number_card/number_card.json @@ -10,6 +10,7 @@ "aggregate_function_based_on", "column_break_2", "document_type", + "is_standard", "is_public", "stats_section", "show_percentage_stats", @@ -95,10 +96,17 @@ "fieldname": "stats_section", "fieldtype": "Section Break", "label": "Stats" + }, + { + "default": "0", + "fieldname": "is_standard", + "fieldtype": "Check", + "label": "Is Standard", + "read_only": 1 } ], "links": [], - "modified": "2020-05-06 19:47:57.753574", + "modified": "2020-05-26 17:30:41.248436", "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..b57913dfac 100644 --- a/frappe/desk/doctype/number_card/number_card.py +++ b/frappe/desk/doctype/number_card/number_card.py @@ -67,6 +67,9 @@ def get_result(doc, to_date=None): filters = frappe.parse_json(doc.filters_json) + if not filters: + filters = [] + if to_date: filters.append([doc.document_type, 'creation', '<', to_date, False]) 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/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index ebe94b4cdb..2e41246cbd 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -842,6 +842,15 @@ frappe.ui.form.Form = class FrappeForm { this.page.clear_primary_action(); } + disable_form() { + this.set_read_only(); + this.fields + .forEach((field) => { + this.set_df_property(field.df.fieldname, "read_only", "1"); + }); + this.disable_save(); + } + handle_save_fail(btn, on_error) { $(btn).prop('disabled', false); if (on_error) { diff --git a/frappe/utils/dashboard.py b/frappe/utils/dashboard.py index b7023427e2..55e9e89464 100644 --- a/frappe/utils/dashboard.py +++ b/frappe/utils/dashboard.py @@ -6,6 +6,9 @@ from frappe import _ from functools import wraps from frappe.utils import add_to_date, cint, get_link_to_form from frappe.modules.import_file import import_doc +import os +from os.path import isfile, join +import ast def cache_source(function): @@ -74,6 +77,18 @@ def get_from_date_from_timespan(to_date, timespan): return add_to_date(to_date, years=years, months=months, days=days, as_datetime=True) +def create_filters_file_after_export(module_name, dashboard_name): + dashboard_path = frappe.get_module_path(\ + module_name,\ + "{module_name}_dashboard".format(module_name=module_name),\ + "{dashboard}".format(dashboard=dashboard_name)\ + ) + charts_path = frappe.get_module_path(module_name, "dashboard charts") + create_filters_file(charts_path, dashboard_path, 'dashboard_chart_filters') + cards_path = frappe.get_module_path(module_name, "number cards") + create_filters_file(cards_path, dashboard_path, 'number_card_filters') + + def sync_dashboards(app=None): """Import, overwrite fixtures from `[app]/fixtures`""" if not cint(frappe.db.get_single_value('System Settings', 'setup_complete')): @@ -86,39 +101,88 @@ def sync_dashboards(app=None): for app_name in apps: print("Updating Dashboard for {app}".format(app=app_name)) for module_name in frappe.local.app_modules.get(app_name) or []: - config = get_config(app_name, module_name) - if config: - frappe.flags.in_import = True - try: - make_records(config.charts, "Dashboard Chart") - make_records(config.number_cards, "Number Card") - make_records(config.dashboards, "Dashboard") - except Exception as e: - frappe.log_error(e, _("Dashboard Import Error")) - finally: - frappe.flags.in_import = False + frappe.flags.in_import = True + setup_dashboards_from_file(app_name, module_name) + frappe.flags.in_import = False -def make_records(config, doctype): - if not config: - return +def setup_dashboards_from_file(app, module): + dashboards_path = frappe.get_module_path(module, "{module}_dashboard".format(module=module)) + chart_filters = {} + card_filters = {} + if os.path.isdir(dashboards_path): + for fname in os.listdir(dashboards_path): + dashboard_path = dashboards_path + '/{}'.format(fname) + if os.path.isdir(dashboard_path): + if fname == '__pycache__': + continue + # create records for all dashboards in the module + make_records(dashboards_path) + chart_filters.update(get_filters(app, module, fname, 'dashboard_chart_filters')) + card_filters.update(get_filters(app, module, fname, 'number_card_filters')) + + charts_path = frappe.get_module_path(module, "dashboard charts") + cards_path = frappe.get_module_path(module, "number cards") + make_records(charts_path, chart_filters) + make_records(cards_path, card_filters) + +def get_filters(app, module, dashboard, filters_file): + module_name = '{app}.{module}.{module}_dashboard.{dashboard}.{filters_file}'.format( + app=app, + module=module, + dashboard=dashboard, + filters_file=filters_file + ) try: - for item in config: - item["doctype"] = doctype - import_doc(item) - frappe.db.commit() - except frappe.DuplicateEntryError: - pass + module = frappe.get_module(module_name) + filters = getattr(module, 'get_filters')() + return filters + except ModuleNotFoundError: + frappe.throw('No Dashboard filters file created') -def get_config(app, module): - try: - module_dashboards = frappe.get_module('{app}.{module}.dashboard_fixtures'.format(app=app, module=module)) - if hasattr(module_dashboards, 'get_data'): - return frappe._dict(module_dashboards.get_data()) - return None - except ImportError: - return None - except Exception as e: - print(_("Failed to import dashboard fixtures for module {module}").format(module=module)) - frappe.log_error(e, _("Dashboard Fixture Import Error")) - return None +def create_filters_file(doc_folder_path, dashboard_path, fname): + filters_dict = get_filters_dict(doc_folder_path) + file_path = '{dashboard_path}/{fname}.py'.format(dashboard_path=dashboard_path, fname=fname) + + with open(file_path, "w") as f: + f.write('''import frappe\n +def get_filters():\n\treturn\\ +''') + f.write(frappe.as_json(filters_dict, indent='\t')) + + +def get_filters_dict(path): + filters_list = [] + for fname in os.listdir(path): + try: + doc_dict = frappe.get_file_json("{path}/{fname}/{fname}.json".format(path=path, fname=fname)) + doc_name = doc_dict['name'] + filters = frappe.parse_json(doc_dict.get('filters_json')) + if isinstance(filters, list): + for f in filters: + if len(f) == 5: + f[4] = cint(f) + doc_filter = '''"{doc_name}": {filters}'''.format(doc_name=doc_name, filters=filters) + filters_list.append(doc_filter) + except FileNotFoundError: + frappe.log_error(message=frappe.get_traceback(), title="Dashboard Import Error") + pass + + filters_dict = ast.literal_eval('{' + ', '.join(filters_list) + '}') + return filters_dict + +def make_records(path, filters=None): + for fname in os.listdir(path): + if os.path.isdir(join(path, fname)): + if fname == '__pycache__': + continue + try: + doc_dict = frappe.get_file_json("{path}/{fname}/{fname}.json".format(path=path, fname=fname)) + doc_name = doc_dict['name'] + doc_dict['is_standard'] = 1 + if filters: + doc_dict['filters_json'] = frappe.as_json(filters[doc_name]) + import_doc(doc_dict) + except FileNotFoundError as e: + frappe.log_error(message=frappe.get_traceback(), title="Dashboard Import Error") + pass