feat: sync dashboards from json files

This commit is contained in:
prssanna 2020-06-04 16:01:49 +05:30
parent cac7cc402f
commit 3aa3832bd3
12 changed files with 217 additions and 51 deletions

View file

@ -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: {

View file

@ -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",

View file

@ -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 <b>{chart_count} Charts</b> and <b>{card_count} Cards</b>').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 = []

View file

@ -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');
});
},

View file

@ -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",

View file

@ -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()

View file

@ -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');

View file

@ -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",

View file

@ -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])

View file

@ -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)

View file

@ -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) {

View file

@ -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