validate_raw_json_grid()   C
last analyzed

Complexity

Conditions 11

Size

Total Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
dl 0
loc 40
rs 5.4
c 1
b 0
f 0

How to fix   Complexity   

Complexity

Complex classes like validate_raw_json_grid() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# -*- coding: utf-8 -*-
2
3
"""
4
flask_jsondash.schema
5
~~~~~~~~~~~~~~~~~~~~~
6
7
The core schema definition and validation rules.
8
9
:copyright: (c) 2016 by Chris Tabor.
10
:license: MIT, see LICENSE for more details.
11
"""
12
13
import json
14
15
import cerberus
16
17
from flask_jsondash.settings import CHARTS_CONFIG
18
19
20
class InvalidSchemaError(ValueError):
21
    """Wrapper exception for specific raising scenarios."""
22
23
24
def get_chart_types():
25
    """Get all available chart 'type' names from core config.
26
27
    Returns:
28
        types (list): A list of all possible chart types, under all families.
29
    """
30
    types = []
31
    charts = [chart['charts'] for chart in CHARTS_CONFIG.values()]
32
    for group in charts:
33
        for chart in group:
34
            types.append(chart[0])
35
    return types
36
37
38
CHART_INPUT_SCHEMA = {
39
    'btn_classes': {
40
        'type': 'list',
41
        'schema': {
42
            'type': 'string',
43
        },
44
    },
45
    'submit_text': {
46
        'type': 'string',
47
    },
48
    'options': {
49
        'type': 'list',
50
        'schema': {
51
            'type': 'dict',
52
            'schema': {
53
                'options': {
54
                    'type': 'list',
55
                    'schema': {
56
                        # E.g. [10, 'Select 10'],
57
                        'type': 'list',
58
                        'minlength': 2,
59
                        'maxlength': 2,
60
                    },
61
                },
62
                'type': {
63
                    'type': 'string',
64
                    'allowed': [
65
                        'number', 'select', 'radio',
66
                        'checkbox', 'text',
67
                        'password',
68
                        # HTML5
69
                        'color',
70
                        'date',
71
                        'datetime-local',
72
                        'month',
73
                        'week',
74
                        'time',
75
                        'email',
76
                        'number',
77
                        'range',
78
                        'search',
79
                        'tel',
80
                        'url',
81
                    ],
82
                    'default': 'text',
83
                },
84
                'name': {
85
                    'type': 'string',
86
                    'required': True,
87
                },
88
                'default': {
89
                    'anyof': [
90
                        {'type': 'string'},
91
                        {'type': 'number'},
92
                        {'type': 'boolean'},
93
                    ],
94
                    'nullable': True,
95
                },
96
                'validator_regex': {
97
                    'nullable': True,
98
                    'type': 'string',
99
                },
100
                'placeholder': {
101
                    'nullable': True,
102
                    'anyof': [
103
                        {'type': 'string'},
104
                        {'type': 'number'},
105
                    ]
106
                },
107
                'label': {
108
                    'type': 'string',
109
                },
110
                'input_classes': {
111
                    'type': 'list',
112
                    'schema': {
113
                        'type': 'string',
114
                    },
115
                },
116
            },
117
        },
118
    },
119
}
120
CHART_SCHEMA = {
121
    'type': 'dict',
122
    'schema': {
123
        'name': {
124
            'type': 'string',
125
            'required': True,
126
        },
127
        'guid': {
128
            'type': 'string',
129
            'required': True,
130
            # 5-"section" regex
131
            # this might not be necessary but it's
132
            # useful to enforce a truly globally unique id,
133
            # especially for usage with the js API.
134
            'regex': (
135
                '[a-zA-Z0-9]+-[a-zA-Z0-9]+-'
136
                '[a-zA-Z0-9]+-[a-zA-Z0-9]+-[a-zA-Z0-9]+'
137
            ),
138
        },
139
        'order': {
140
            'type': 'number',
141
            'nullable': True,
142
            'default': 0,
143
        },
144
        'row': {
145
            'type': 'number',
146
            'nullable': True,
147
            'default': 0,
148
        },
149
        'refresh': {
150
            'type': 'boolean',
151
            'nullable': True,
152
        },
153
        'refreshInterval': {
154
            'type': 'number',
155
            'nullable': True,
156
        },
157
        'height': {
158
            'type': 'number',
159
            'required': True,
160
        },
161
        'width': {
162
            'anyof': [
163
                {'type': 'string'},
164
                {'type': 'number'},
165
            ],
166
            # TODO: check this works
167
            # This regex allows the overloading
168
            # of this property to allow for
169
            # grid widths (e.g. "col-8") vs number widths.
170
            'regex': '(col-[0-9]+)|([0-9]+)',
171
            'required': True,
172
        },
173
        'dataSource': {
174
            'type': 'string',
175
            'required': True,
176
        },
177
        'family': {
178
            'type': 'string',
179
            'required': True,
180
            'allowed': list(CHARTS_CONFIG.keys()),
181
        },
182
        'key': {
183
            'type': 'string',
184
            'required': False,
185
        },
186
        'type': {
187
            'type': 'string',
188
            'required': True,
189
            'allowed': get_chart_types(),
190
        },
191
        'inputs': {
192
            'type': 'dict',
193
            'schema': CHART_INPUT_SCHEMA
194
        },
195
    },
196
}
197
DASHBOARD_SCHEMA = {
198
    'name': {
199
        'type': 'string',
200
        'required': True,
201
    },
202
    'id': {
203
        'type': 'string',
204
        'required': True,
205
        'regex': (
206
            '[a-zA-Z0-9]+-[a-zA-Z0-9]+-'
207
            '[a-zA-Z0-9]+-[a-zA-Z0-9]+-[a-zA-Z0-9]+'
208
        ),
209
    },
210
    'date': {
211
        'type': 'string',
212
        'required': True,
213
    },
214
    'layout': {
215
        'type': 'string',
216
        'required': True,
217
        'allowed': ['freeform', 'grid'],
218
    },
219
    'category': {
220
        'type': 'string',
221
        'required': False,
222
    },
223
    'modules': {
224
        'type': 'list',
225
        'schema': CHART_SCHEMA,
226
        'required': True,
227
    },
228
}
229
230
231
def validate(conf):
232
    """Validate a json conf."""
233
    v = cerberus.Validator(DASHBOARD_SCHEMA, allow_unknown=True)
234
    valid = v.validate(conf)
235
    if not valid:
236
        return v.errors
237
238
239
def is_consecutive_rows(lst):
240
    """Check if a list of integers is consecutive.
241
242
    Args:
243
        lst (list): The list of integers.
244
245
    Returns:
246
        True/False: If the list contains consecutive integers.
247
248
    Originally taken from and modified:
249
        http://stackoverflow.com/
250
            questions/40091617/test-for-consecutive-numbers-in-list
251
    """
252
    assert 0 not in lst, '0th index is invalid!'
253
    lst = list(set(lst))
254
    if not lst:
255
        return True
256
    setl = set(lst)
257
    return len(lst) == len(setl) and setl == set(range(min(lst), max(lst) + 1))
258
259
260
def validate_raw_json_grid(conf):
261
    """Grid mode specific validations.
262
263
    Args:
264
        conf (dict): The dashboard configuration.
265
266
    Raises:
267
        InvalidSchemaError: If there are any issues with the schema
268
269
    Returns:
270
        None: If no errors were found.
271
    """
272
    layout = conf.get('layout', 'freeform')
273
    fixed_only_required = ['row']
274
    rows = []
275
    modules = conf.get('modules', [])
276
    valid_cols = ['col-{}'.format(i) for i in range(1, 13)]
277
    if not modules:
278
        return
279
    for module in modules:
280
        try:
281
            rows.append(int(module.get('row')))
282
        except TypeError:
283
            raise InvalidSchemaError(
284
                'Invalid row value for module "{}"'.format(module.get('name')))
285
    if not is_consecutive_rows(rows):
286
        raise InvalidSchemaError(
287
            'Row order is not consecutive: "{}"!'.format(sorted(rows)))
288
    for module in modules:
289
        width = module.get('width')
290
        if width not in valid_cols:
291
            raise InvalidSchemaError(
292
                'Invalid width for grid format: "{}"'.format(width))
293
        for field in fixed_only_required:
294
            if field not in module and layout == 'grid':
295
                ident = module.get('name', module)
296
                raise InvalidSchemaError(
297
                    'Invalid JSON. "{}" must be '
298
                    'included in "{}" for '
299
                    'fixed grid layouts'.format(field, ident))
300
301
302
def validate_raw_json(jsonstr, **overrides):
303
    """Validate the raw json for a config.
304
305
    Args:
306
        jsonstr (str): The raw json configuration
307
        **overrides: Any key/value pairs to override in the config.
308
            Used only for setting default values that the user should
309
            never enter but are required to validate the schema.
310
311
    Raises:
312
        InvalidSchemaError: If there are any issues with the schema
313
314
    Returns:
315
        data (dict): The parsed configuration data
316
    """
317
    data = json.loads(jsonstr)
318
    data.update(**overrides)
319
    layout = data.get('layout', 'freeform')
320
321
    if layout == 'grid':
322
        validate_raw_json_grid(data)
323
    else:
324
        for module in data.get('modules', []):
325
            width = module.get('width')
326
            try:
327
                int(width)
328
            except (TypeError, ValueError):
329
                raise InvalidSchemaError(
330
                    'Invalid value for width in `freeform` layout.')
331
            if module.get('row') is not None:
332
                raise InvalidSchemaError(
333
                    'Cannot mix `row` with `freeform` layout.')
334
    results = validate(data)
335
    if results is not None:
336
        raise InvalidSchemaError(results)
337
    return data
338