1
|
|
|
# -*- coding: utf-8 -*- |
2
|
|
|
|
3
|
|
|
""" |
4
|
|
|
flask_jsondash.model_factories |
5
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
6
|
|
|
|
7
|
|
|
Data generation utilities for all charts and dashboards. |
8
|
|
|
|
9
|
|
|
:copyright: (c) 2016 by Chris Tabor. |
10
|
|
|
:license: MIT, see LICENSE for more details. |
11
|
|
|
""" |
12
|
|
|
|
13
|
|
|
import os |
14
|
|
|
import json |
15
|
|
|
from datetime import datetime as dt |
16
|
|
|
from random import choice, randrange |
17
|
|
|
from uuid import uuid1 |
18
|
|
|
|
19
|
|
|
import click |
20
|
|
|
from werkzeug.datastructures import ImmutableMultiDict |
21
|
|
|
|
22
|
|
|
from flask_jsondash import db, settings |
23
|
|
|
|
24
|
|
|
adapter = db.get_db_handler() |
25
|
|
|
|
26
|
|
|
|
27
|
|
|
def get_random_group(): |
28
|
|
|
"""Return a random namespaced group of charts. |
29
|
|
|
|
30
|
|
|
Returns: |
31
|
|
|
str: A random chart name. |
32
|
|
|
|
33
|
|
|
Examples: |
34
|
|
|
>>> get_random_group() |
35
|
|
|
>>> {'charts': [...], 'dependencies', 'js_url': '...', ...} |
36
|
|
|
""" |
37
|
|
|
return choice(list(settings.CHARTS_CONFIG.values())) |
38
|
|
|
|
39
|
|
|
|
40
|
|
|
def get_random_chart(group): |
41
|
|
|
"""Get a random chart from a specific chart sub-group. |
42
|
|
|
|
43
|
|
|
Args: |
44
|
|
|
group (dict): A group from the global chart settings config. |
45
|
|
|
""" |
46
|
|
|
return choice(list(group['charts'])) |
47
|
|
|
|
48
|
|
|
|
49
|
|
|
def load_fixtures(path): |
50
|
|
|
"""Generate all dashboards using any given schemas. |
51
|
|
|
|
52
|
|
|
Note: this does NOT account for nested folders, |
53
|
|
|
only all json in the given directory. |
54
|
|
|
|
55
|
|
|
Args: |
56
|
|
|
name (path): The relative folder path for all json configs |
57
|
|
|
""" |
58
|
|
|
click.echo('Loading fixtures for path ' + path) |
59
|
|
|
files = [f for f in os.listdir(path) if f.endswith('.json')] |
60
|
|
|
for file in files: |
61
|
|
|
file = '{}/{}/{}'.format(os.getcwd(), path, file) |
62
|
|
|
click.echo('Loading ' + file) |
63
|
|
|
with open(file, 'r') as fixture: |
64
|
|
|
data = json.loads(fixture.read()) |
65
|
|
|
adapter.create(data=data) |
66
|
|
|
|
67
|
|
|
|
68
|
|
|
def dump_fixtures(path, delete_after=False): |
69
|
|
|
"""Generate fixture data (json) from existing records in db. |
70
|
|
|
|
71
|
|
|
Args: |
72
|
|
|
name (path): The folder path to save configs in (must exist first!) |
73
|
|
|
delete_after (bool, optional): Delete records after dumping fixtures. |
74
|
|
|
""" |
75
|
|
|
click.echo('Saving db as fixtures to: ' + path) |
76
|
|
|
# If an error occured, don't delete any records |
77
|
|
|
errors = [] |
78
|
|
|
# Allow absolute OR relative paths. |
79
|
|
|
cwd = '' if path.startswith('/') else os.getcwd() |
80
|
|
|
dashboards = list(adapter.read()) |
81
|
|
|
if not dashboards: |
82
|
|
|
click.echo('Nothing to dump.') |
83
|
|
|
return |
84
|
|
|
for dashboard in dashboards: |
85
|
|
|
# Mongo id is not serializeable |
86
|
|
|
if '_id' in dashboard: |
87
|
|
|
dashboard.pop('_id') |
88
|
|
|
# Update date records |
89
|
|
|
dashboard.update(date=str(dashboard['date'])) |
90
|
|
|
name = '{}-{}'.format(dashboard['name'], dashboard['id']) |
91
|
|
|
name = name.replace(' ', '-').lower() |
92
|
|
|
fullpath = '{}/{}/{}.json'.format(cwd, path, name) |
93
|
|
|
click.echo('Saving fixture: ' + fullpath) |
94
|
|
|
try: |
95
|
|
|
with open(fullpath, 'w') as fixture: |
96
|
|
|
fixture.write(json.dumps(dashboard, indent=4)) |
97
|
|
|
except Exception as e: |
98
|
|
|
click.echo(e) |
99
|
|
|
errors.append(fullpath) |
100
|
|
|
continue |
101
|
|
|
if delete_after and not errors: |
102
|
|
|
adapter.delete_all() |
103
|
|
|
if errors: |
104
|
|
|
click.echo('The following records could not be dumped: {}'.format( |
105
|
|
|
errors |
106
|
|
|
)) |
107
|
|
|
|
108
|
|
|
|
109
|
|
|
def make_fake_dashboard(name='Random chart', max_charts=10): |
110
|
|
|
"""Generate fake dashboard data with a specific number of random charts. |
111
|
|
|
|
112
|
|
|
Args: |
113
|
|
|
name (str): The name of the new dashboard (default: {'Random chart'}) |
114
|
|
|
max_charts (int): Max number of charts to make (default: {10}) |
115
|
|
|
|
116
|
|
|
Returns: |
117
|
|
|
dict: The chart configuration. |
118
|
|
|
""" |
119
|
|
|
charts = ImmutableMultiDict([ |
120
|
|
|
make_fake_chart_data() for _ in range(max_charts)] |
121
|
|
|
) |
122
|
|
|
return dict( |
123
|
|
|
name=name, |
124
|
|
|
created_by='global', |
125
|
|
|
date=dt.now(), |
126
|
|
|
category=choice(list(settings.CHARTS_CONFIG.keys())), |
127
|
|
|
modules=db.format_charts(charts), |
128
|
|
|
id=str(uuid1()), |
129
|
|
|
layout='freeform', |
130
|
|
|
) |
131
|
|
|
|
132
|
|
|
|
133
|
|
|
def make_fake_chart_data(**kwargs): |
134
|
|
|
"""Return chart data in required format. |
135
|
|
|
|
136
|
|
|
Args: |
137
|
|
|
name (None, optional): The name of the chart. |
138
|
|
|
height (None, optional): The height of the chart. |
139
|
|
|
width (None, optional): The width of the chart. |
140
|
|
|
data_source (None, optional): The data source (url) for the chart. |
141
|
|
|
|
142
|
|
|
Returns: |
143
|
|
|
tuple: A 2-tuple of type (string_label, jsonstring) for the chart. |
144
|
|
|
|
145
|
|
|
Examples: |
146
|
|
|
>>> make_fake_chart_date(width=10, height=10, name='foo') |
147
|
|
|
>>> ('foo', '{"name": "foo", "height": 10, "width": 10}') |
148
|
|
|
""" |
149
|
|
|
_uuid = str(uuid1()) |
150
|
|
|
# All of these chart types have example endpoints to use locally. |
151
|
|
|
chart = choice(['bar', 'line', 'step', 'area']) |
152
|
|
|
will_use_inputs = randrange(1, 100) > 50 |
153
|
|
|
url = 'http://127.0.0.1:5004/bar' |
154
|
|
|
config = dict( |
155
|
|
|
name=kwargs.get('name'), |
156
|
|
|
width=kwargs.get('width', randrange(200, 2000)), |
157
|
|
|
height=kwargs.get('height', randrange(200, 2000)), |
158
|
|
|
type=chart, |
159
|
|
|
family='C3', |
160
|
|
|
dataSource=kwargs.get('data_source', url), |
161
|
|
|
guid=_uuid, |
162
|
|
|
) |
163
|
|
|
if will_use_inputs: |
164
|
|
|
config.update( |
165
|
|
|
inputs=dict( |
166
|
|
|
btn_classes=['btn', 'btn-info', 'btn-sm'], |
167
|
|
|
submit_text='Submit', |
168
|
|
|
help_text='Change a value', |
169
|
|
|
options=[ |
170
|
|
|
dict(type='number', name='range', label='Number'), |
171
|
|
|
] |
172
|
|
|
) |
173
|
|
|
) |
174
|
|
|
return ( |
175
|
|
|
'module_{}'.format(_uuid), |
176
|
|
|
json.dumps(config) |
177
|
|
|
) |
178
|
|
|
|
179
|
|
|
|
180
|
|
|
@click.command() |
181
|
|
|
@click.option('--records', |
182
|
|
|
default=10, |
183
|
|
|
help='Number of records to insert fake dashboard data into DB.') |
184
|
|
|
@click.option('--max-charts', |
185
|
|
|
default=5, |
186
|
|
|
help='Number of charts per dashboard to create.') |
187
|
|
|
@click.option('--fixtures', |
188
|
|
|
default=None, |
189
|
|
|
help='A path to load existing configurations from.') |
190
|
|
|
@click.option('--dump', |
191
|
|
|
default=None, |
192
|
|
|
help='A path to dump db records, as json dashboard configs.') |
193
|
|
|
@click.option('--delete', |
194
|
|
|
is_flag=True, |
195
|
|
|
default=False, |
196
|
|
|
help='Determine if the database records should be deleted') |
197
|
|
|
def insert_dashboards(records, max_charts, fixtures, dump, delete): |
198
|
|
|
"""Insert a number of dashboard records into the database.""" |
199
|
|
|
if fixtures is not None: |
200
|
|
|
return load_fixtures(fixtures) |
201
|
|
|
if dump is not None: |
202
|
|
|
return dump_fixtures(dump, delete_after=delete) |
203
|
|
|
if dump is None and delete: |
204
|
|
|
click.echo('Deleting all records!') |
205
|
|
|
adapter.delete_all() |
206
|
|
|
return |
207
|
|
|
for i in range(records): |
208
|
|
|
data = make_fake_dashboard( |
209
|
|
|
name='Test chart #{}'.format(i), |
210
|
|
|
max_charts=max_charts) |
211
|
|
|
adapter.create(data=data) |
212
|
|
|
|
213
|
|
|
|
214
|
|
|
def delete_all(): |
215
|
|
|
"""Delete all dashboards.""" |
216
|
|
|
adapter.delete_all() |
217
|
|
|
|
218
|
|
|
|
219
|
|
|
if __name__ == '__main__': |
220
|
|
|
insert_dashboards() |
221
|
|
|
|