ooui 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ooui-0.1.0/LICENSE +21 -0
- ooui-0.1.0/PKG-INFO +12 -0
- ooui-0.1.0/README.md +2 -0
- ooui-0.1.0/ooui/__init__.py +0 -0
- ooui-0.1.0/ooui/graph/__init__.py +30 -0
- ooui-0.1.0/ooui/graph/axis.py +100 -0
- ooui-0.1.0/ooui/graph/base.py +31 -0
- ooui-0.1.0/ooui/graph/chart.py +20 -0
- ooui-0.1.0/ooui/graph/fields.py +116 -0
- ooui-0.1.0/ooui/graph/indicator.py +39 -0
- ooui-0.1.0/ooui/graph/processor.py +186 -0
- ooui-0.1.0/ooui/graph/timerange.py +300 -0
- ooui-0.1.0/ooui/helpers/__init__.py +25 -0
- ooui-0.1.0/ooui.egg-info/PKG-INFO +12 -0
- ooui-0.1.0/ooui.egg-info/SOURCES.txt +18 -0
- ooui-0.1.0/ooui.egg-info/dependency_links.txt +1 -0
- ooui-0.1.0/ooui.egg-info/requires.txt +3 -0
- ooui-0.1.0/ooui.egg-info/top_level.txt +1 -0
- ooui-0.1.0/setup.cfg +4 -0
- ooui-0.1.0/setup.py +23 -0
ooui-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 GISCE-TI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ooui-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ooui
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Open Object User Interface
|
|
5
|
+
Home-page: https://github.com/gisce/python-ooui
|
|
6
|
+
Author: GISCE
|
|
7
|
+
Author-email: devel@gisce.net
|
|
8
|
+
License: MIT
|
|
9
|
+
Provides: ooui
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
|
|
12
|
+
Open Object User Interface for GISCE-ERP
|
ooui-0.1.0/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import absolute_import, unicode_literals
|
|
2
|
+
from lxml import etree
|
|
3
|
+
from ooui.graph.indicator import GraphIndicator
|
|
4
|
+
from ooui.graph.chart import GraphChart
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
GRAPH_TYPES = {
|
|
8
|
+
'indicator': GraphIndicator,
|
|
9
|
+
'line': GraphChart,
|
|
10
|
+
'pie': GraphChart,
|
|
11
|
+
'bar': GraphChart,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_graph(xml):
|
|
16
|
+
"""
|
|
17
|
+
Parse a graph from an XML string.
|
|
18
|
+
:param xml:
|
|
19
|
+
:return:
|
|
20
|
+
:rtype ooui.graph.Graph
|
|
21
|
+
"""
|
|
22
|
+
tree = etree.fromstring(xml)
|
|
23
|
+
graph = tree.xpath('//graph')[0]
|
|
24
|
+
|
|
25
|
+
graph_type = graph.get("type")
|
|
26
|
+
|
|
27
|
+
if not graph_type or graph_type not in GRAPH_TYPES:
|
|
28
|
+
raise ValueError("{} is not a valid graph".format(graph_type))
|
|
29
|
+
|
|
30
|
+
return GRAPH_TYPES[graph_type](graph_type, graph)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
class GraphAxis(object):
|
|
2
|
+
AXIS_OPTIONS = ('x', 'y')
|
|
3
|
+
|
|
4
|
+
def __init__(self, name, axis):
|
|
5
|
+
if axis not in self.AXIS_OPTIONS:
|
|
6
|
+
raise ValueError("Invalid axis value. Must be 'x' or 'y'.")
|
|
7
|
+
|
|
8
|
+
self._name = name
|
|
9
|
+
self._axis = axis
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def name(self):
|
|
13
|
+
return self._name
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def axis(self):
|
|
17
|
+
return self._axis
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GraphXAxis(GraphAxis):
|
|
21
|
+
def __init__(self, name):
|
|
22
|
+
# Crida el constructor de GraphAxis amb l'eix 'x' fixat
|
|
23
|
+
super(GraphXAxis, self).__init__(name, 'x')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class GraphYAxis(GraphAxis):
|
|
27
|
+
OPERATOR_OPTIONS = ('count', '+', '-', '*', 'min', 'max', 'avg')
|
|
28
|
+
|
|
29
|
+
def __init__(self, name, operator, label=None, stacked=None):
|
|
30
|
+
super(GraphYAxis, self).__init__(name, 'y')
|
|
31
|
+
|
|
32
|
+
if operator not in self.OPERATOR_OPTIONS:
|
|
33
|
+
raise ValueError("Invalid operator value.")
|
|
34
|
+
|
|
35
|
+
self._operator = operator
|
|
36
|
+
self._label = label
|
|
37
|
+
self._stacked = stacked
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def operator(self):
|
|
41
|
+
return self._operator
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def label(self):
|
|
45
|
+
return self._label
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def stacked(self):
|
|
49
|
+
return self._stacked
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse_xy_axis(nodes):
|
|
53
|
+
x_axis = None
|
|
54
|
+
y_axes = []
|
|
55
|
+
|
|
56
|
+
for child in nodes:
|
|
57
|
+
if child.tag == "field":
|
|
58
|
+
axis = child.get("axis")
|
|
59
|
+
name = child.get("name")
|
|
60
|
+
operator = child.get("operator")
|
|
61
|
+
label = child.get("label")
|
|
62
|
+
stacked = child.get("stacked")
|
|
63
|
+
|
|
64
|
+
if not axis:
|
|
65
|
+
raise ValueError("Field {} doesn't have an axis".format(name))
|
|
66
|
+
|
|
67
|
+
if not name:
|
|
68
|
+
raise ValueError("Missing name attribute for field")
|
|
69
|
+
|
|
70
|
+
if axis == "x":
|
|
71
|
+
x_axis = GraphXAxis(name)
|
|
72
|
+
elif axis == "y":
|
|
73
|
+
y_axes.append(GraphYAxis(name, operator, label, stacked))
|
|
74
|
+
|
|
75
|
+
if not x_axis:
|
|
76
|
+
raise ValueError("No x axis found")
|
|
77
|
+
|
|
78
|
+
if not y_axes:
|
|
79
|
+
raise ValueError("No y axis found. At least one y axis is required")
|
|
80
|
+
|
|
81
|
+
return {"x": x_axis, "y": y_axes}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_y_axis_fieldname(y_axis, fields):
|
|
85
|
+
"""
|
|
86
|
+
Retrieve the field name for a Y-axis based on the `GraphYAxis` object
|
|
87
|
+
and field definitions.
|
|
88
|
+
|
|
89
|
+
:param GraphYAxis y_axis: An object representing the Y-axis.
|
|
90
|
+
:param dict fields: A dictionary containing the field definitions.
|
|
91
|
+
|
|
92
|
+
:rtype: str
|
|
93
|
+
:returns: The name of the Y-axis field.
|
|
94
|
+
"""
|
|
95
|
+
field_props = fields.get(y_axis.name, {})
|
|
96
|
+
|
|
97
|
+
if 'string' in field_props and field_props['string']:
|
|
98
|
+
return field_props['string']
|
|
99
|
+
|
|
100
|
+
return y_axis.name
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
class Graph(object):
|
|
4
|
+
def __init__(self, element):
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
:param element: lxml.etree._Element
|
|
8
|
+
"""
|
|
9
|
+
self._string = element.get('string')
|
|
10
|
+
self._timerange = element.get('timerange', None)
|
|
11
|
+
|
|
12
|
+
interval = element.get('interval', None)
|
|
13
|
+
self._interval = int(interval) if interval is not None else 1
|
|
14
|
+
|
|
15
|
+
self._type = None
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def string(self):
|
|
19
|
+
return self._string
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def timerange(self):
|
|
23
|
+
return self._timerange
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def interval(self):
|
|
27
|
+
return self._interval
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def type(self):
|
|
31
|
+
return self._type
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from ooui.graph.base import Graph
|
|
2
|
+
from ooui.graph.axis import parse_xy_axis
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class GraphChart(Graph):
|
|
6
|
+
def __init__(self, graph_type, element):
|
|
7
|
+
super(GraphChart, self).__init__(element)
|
|
8
|
+
|
|
9
|
+
self._type = graph_type
|
|
10
|
+
xy_axis = parse_xy_axis(element)
|
|
11
|
+
self._x = xy_axis['x']
|
|
12
|
+
self._y = xy_axis['y']
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def x(self):
|
|
16
|
+
return self._x
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def y(self):
|
|
20
|
+
return self._y
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from functools import reduce
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_fields_to_retrieve(ooui):
|
|
5
|
+
"""
|
|
6
|
+
Returns a list of fields to retrieve from the ooui
|
|
7
|
+
:param ooui: Graph instance
|
|
8
|
+
:type ooui: ooui.graph.chart.GraphChart
|
|
9
|
+
:return: list of fields to retrieve
|
|
10
|
+
:rtype: list[str]
|
|
11
|
+
"""
|
|
12
|
+
x_field = ooui.x.name
|
|
13
|
+
fields = [x_field]
|
|
14
|
+
|
|
15
|
+
if not ooui.y:
|
|
16
|
+
return []
|
|
17
|
+
|
|
18
|
+
for y in ooui.y:
|
|
19
|
+
if y.operator != 'count' and y.name not in fields:
|
|
20
|
+
fields.append(y.name)
|
|
21
|
+
|
|
22
|
+
if y.label and y.label not in fields:
|
|
23
|
+
fields.append(y.label)
|
|
24
|
+
|
|
25
|
+
return fields
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_value_and_label_for_field(fields, values, field_name):
|
|
29
|
+
"""
|
|
30
|
+
Retrieve the value and label for a specific field.
|
|
31
|
+
|
|
32
|
+
:param dict fields: A dictionary containing the field definitions.
|
|
33
|
+
The keys are field names and the values are their details.
|
|
34
|
+
:param dict values: A dictionary containing the field values.
|
|
35
|
+
The keys are field names, and the values are the associated values.
|
|
36
|
+
:param str field_name: The name of the field for which to obtain the value
|
|
37
|
+
and label.
|
|
38
|
+
|
|
39
|
+
:rtype: dict
|
|
40
|
+
:returns: A dictionary with the keys `value` and `label`:
|
|
41
|
+
- `value`: The field value or `False` if not found.
|
|
42
|
+
- `label`: The field label or `None` if not found.
|
|
43
|
+
|
|
44
|
+
:raises ValueError: If the specified field is not found in the `fields`
|
|
45
|
+
dictionary.
|
|
46
|
+
"""
|
|
47
|
+
x_field_data = fields.get(field_name)
|
|
48
|
+
value = values.get(field_name)
|
|
49
|
+
|
|
50
|
+
if not x_field_data:
|
|
51
|
+
raise ValueError("Field {} not found".format(field_name))
|
|
52
|
+
|
|
53
|
+
if x_field_data['type'] == 'many2one':
|
|
54
|
+
if not value:
|
|
55
|
+
return {'value': False, 'label': None}
|
|
56
|
+
return {'value': value[0], 'label': value[1]}
|
|
57
|
+
|
|
58
|
+
elif x_field_data['type'] == 'selection':
|
|
59
|
+
selection_values = x_field_data['selection']
|
|
60
|
+
value_pair = next((pair for pair in selection_values if pair[0] == value), None)
|
|
61
|
+
|
|
62
|
+
if not value_pair:
|
|
63
|
+
return {'value': False, 'label': None}
|
|
64
|
+
|
|
65
|
+
return {'value': value, 'label': value_pair[1]}
|
|
66
|
+
|
|
67
|
+
return {'value': value, 'label': value}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_value_for_operator(operator, values):
|
|
71
|
+
"""
|
|
72
|
+
Retrieve the result of applying an operator on a list of values.
|
|
73
|
+
|
|
74
|
+
:param str operator: The operator to be applied.
|
|
75
|
+
Possible values include "count", "+", "-", "*", "avg", "min", "max".
|
|
76
|
+
:param list values: A list of numerical values on which to apply the
|
|
77
|
+
operator.
|
|
78
|
+
|
|
79
|
+
:rtype: float or int
|
|
80
|
+
:returns: The result of applying the operator to the values.
|
|
81
|
+
|
|
82
|
+
:raises ValueError: If an unsupported operator is provided or if the
|
|
83
|
+
values list is empty for certain operators.
|
|
84
|
+
"""
|
|
85
|
+
if operator == "count":
|
|
86
|
+
return len(values)
|
|
87
|
+
elif operator == "+":
|
|
88
|
+
return round_number(sum(values))
|
|
89
|
+
elif operator == "-":
|
|
90
|
+
return round_number(reduce(lambda x, y: x - y, values))
|
|
91
|
+
elif operator == "*":
|
|
92
|
+
return round_number(reduce(lambda x, y: x * y, values))
|
|
93
|
+
elif operator == "avg":
|
|
94
|
+
if not values:
|
|
95
|
+
return 0
|
|
96
|
+
total_sum = sum(values)
|
|
97
|
+
avg = total_sum / len(values)
|
|
98
|
+
return round_number(avg)
|
|
99
|
+
elif operator == "min":
|
|
100
|
+
return min(values)
|
|
101
|
+
elif operator == "max":
|
|
102
|
+
return max(values)
|
|
103
|
+
else:
|
|
104
|
+
raise ValueError("Unsupported operator: {}".format(operator))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def round_number(num):
|
|
108
|
+
"""
|
|
109
|
+
Round a number to two decimal places.
|
|
110
|
+
|
|
111
|
+
:param float or int num: The number to be rounded.
|
|
112
|
+
|
|
113
|
+
:rtype: float
|
|
114
|
+
:returns: The number rounded to two decimal places.
|
|
115
|
+
"""
|
|
116
|
+
return round(num * 100) / 100
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from ooui.graph.base import Graph
|
|
2
|
+
from ooui.helpers import parse_bool_attribute, replace_entities
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class GraphIndicator(Graph):
|
|
6
|
+
def __init__(self, graph_type, element):
|
|
7
|
+
# Inicia la classe base Graph
|
|
8
|
+
super(GraphIndicator, self).__init__(element)
|
|
9
|
+
|
|
10
|
+
self._type = graph_type
|
|
11
|
+
self._color = replace_entities(element.get('color')) if element.get(
|
|
12
|
+
'color') else None
|
|
13
|
+
self._icon = replace_entities(element.get('icon')) if element.get(
|
|
14
|
+
'icon') else None
|
|
15
|
+
self._suffix = element.get('suffix') if element.get('suffix') else None
|
|
16
|
+
self._total_domain = replace_entities(
|
|
17
|
+
element.get('totalDomain')) if element.get('totalDomain') else None
|
|
18
|
+
self._show_percent = parse_bool_attribute(
|
|
19
|
+
element.get('showPercent')) if element.get('showPercent') else False
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def color(self):
|
|
23
|
+
return self._color
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def icon(self):
|
|
27
|
+
return self._icon
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def total_domain(self):
|
|
31
|
+
return self._total_domain
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def show_percent(self):
|
|
35
|
+
return self._show_percent
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def suffix(self):
|
|
39
|
+
return self._suffix
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
from __future__ import absolute_import, unicode_literals
|
|
2
|
+
from ooui.graph.fields import get_value_and_label_for_field, get_value_for_operator
|
|
3
|
+
from ooui.graph.axis import get_y_axis_fieldname
|
|
4
|
+
from ooui.graph.timerange import process_timerange_data
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def process_graph_data(ooui, values, fields, options=None):
|
|
8
|
+
"""
|
|
9
|
+
Process graph data by grouping and sorting the values according to the
|
|
10
|
+
specified X and Y axes.
|
|
11
|
+
|
|
12
|
+
:param ooui: A GraphChart-like object containing chart information.
|
|
13
|
+
:type ooui: ooui.graph.GraphChart
|
|
14
|
+
:param list values: A list of dictionaries representing the original data.
|
|
15
|
+
:param dict fields: A dictionary of field definitions.
|
|
16
|
+
:param dict options: Optional additional options for processing graph data.
|
|
17
|
+
|
|
18
|
+
:rtype: dict
|
|
19
|
+
:returns: A dictionary containing the final processed data and flags like
|
|
20
|
+
isGroup and isStack.
|
|
21
|
+
"""
|
|
22
|
+
if options is None:
|
|
23
|
+
options = {}
|
|
24
|
+
|
|
25
|
+
values_grouped_by_x = get_values_grouped_by_field(
|
|
26
|
+
ooui.x.name, fields, values
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
data = []
|
|
30
|
+
|
|
31
|
+
# Iterate through the y-axis items
|
|
32
|
+
for y_field in ooui.y:
|
|
33
|
+
for x_value, group in values_grouped_by_x.items():
|
|
34
|
+
x_label = group['label']
|
|
35
|
+
objects_for_x_value = group['entries']
|
|
36
|
+
|
|
37
|
+
if not y_field.label:
|
|
38
|
+
values_for_y_field = get_values_for_y_field(
|
|
39
|
+
objects_for_x_value, y_field.name, fields
|
|
40
|
+
)
|
|
41
|
+
final_value = get_value_for_operator(
|
|
42
|
+
y_field.operator, values_for_y_field
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
data.append({
|
|
46
|
+
'x': x_label or False,
|
|
47
|
+
'value': final_value,
|
|
48
|
+
'type': get_y_axis_fieldname(y_field, fields),
|
|
49
|
+
'operator': y_field.operator,
|
|
50
|
+
'stacked': y_field.stacked
|
|
51
|
+
})
|
|
52
|
+
else:
|
|
53
|
+
values_grouped_by_y_label = get_values_grouped_by_field(
|
|
54
|
+
y_field.label, fields, objects_for_x_value
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
for y_unique_value, grouped_entries in values_grouped_by_y_label.items():
|
|
58
|
+
entries = grouped_entries['entries']
|
|
59
|
+
label = grouped_entries['label']
|
|
60
|
+
|
|
61
|
+
values_for_y_field = get_values_for_y_field(
|
|
62
|
+
entries, y_field.name, fields
|
|
63
|
+
)
|
|
64
|
+
final_value = get_value_for_operator(
|
|
65
|
+
y_field.operator, values_for_y_field
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
data.append({
|
|
69
|
+
'x': x_label or False,
|
|
70
|
+
'value': final_value,
|
|
71
|
+
'type': label,
|
|
72
|
+
'operator': y_field.operator,
|
|
73
|
+
'stacked': y_field.stacked
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
# Check if data should be flagged as grouped or stacked
|
|
77
|
+
is_group = any(y.label is not None for y in ooui.y)
|
|
78
|
+
is_stack = any(y.stacked is not None for y in ooui.y)
|
|
79
|
+
|
|
80
|
+
# Sort the data by the x-axis
|
|
81
|
+
sorted_data = sorted(data, key=lambda x: x['x'] or "")
|
|
82
|
+
|
|
83
|
+
adjusted_stacked_data = sorted_data[:]
|
|
84
|
+
if is_stack and len([y for y in ooui.y if y.stacked is not None]) > 1:
|
|
85
|
+
adjusted_stacked_data = [
|
|
86
|
+
dict(
|
|
87
|
+
entry,
|
|
88
|
+
type="{} - {}".format(entry['type'], entry['stacked'])
|
|
89
|
+
) for entry in sorted_data
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
adjusted_uninformed_data = adjusted_stacked_data[:]
|
|
93
|
+
if ooui.type == 'pie' and any(entry['x'] is False for entry in adjusted_stacked_data):
|
|
94
|
+
adjusted_uninformed_data = [
|
|
95
|
+
dict(
|
|
96
|
+
entry,
|
|
97
|
+
x=options.get('uninformedString', 'Not informed') if entry['x'] is False else entry['x']
|
|
98
|
+
) for entry in adjusted_stacked_data
|
|
99
|
+
]
|
|
100
|
+
else:
|
|
101
|
+
adjusted_uninformed_data = [
|
|
102
|
+
entry for entry in adjusted_stacked_data if entry['x'] is not False
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
# Fill gaps if a timerange is defined
|
|
106
|
+
final_data = adjusted_uninformed_data
|
|
107
|
+
if ooui.timerange:
|
|
108
|
+
final_data = process_timerange_data(
|
|
109
|
+
final_data, ooui.timerange, ooui.interval
|
|
110
|
+
)
|
|
111
|
+
elif ooui.type == 'pie':
|
|
112
|
+
final_data = sorted(
|
|
113
|
+
adjusted_uninformed_data, key=lambda x: x['value'], reverse=True
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
'data': final_data,
|
|
118
|
+
'isGroup': is_stack or is_group,
|
|
119
|
+
'isStack': is_stack
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get_values_for_y_field(entries, field_name, fields):
|
|
124
|
+
"""
|
|
125
|
+
Retrieve labels for a specified field across multiple entries.
|
|
126
|
+
|
|
127
|
+
:param list entries: A list of dictionaries representing the entries.
|
|
128
|
+
:param str field_name: The name of the field for which to retrieve labels.
|
|
129
|
+
:param dict fields: A dictionary containing the field definitions.
|
|
130
|
+
:param func get_value_and_label_for_field: Function to retrieve value and
|
|
131
|
+
label for a field.
|
|
132
|
+
|
|
133
|
+
:rtype: list
|
|
134
|
+
:returns: A list containing the labels corresponding to the specified field.
|
|
135
|
+
"""
|
|
136
|
+
return [
|
|
137
|
+
get_value_and_label_for_field(fields, entry, field_name)['label']
|
|
138
|
+
for entry in entries
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get_values_grouped_by_field(field_name, fields, values):
|
|
143
|
+
"""
|
|
144
|
+
Group values by a specific field.
|
|
145
|
+
|
|
146
|
+
:param str field_name: The name of the field by which to group values.
|
|
147
|
+
:param dict fields: A dictionary containing field definitions.
|
|
148
|
+
:param list values: A list of dictionaries representing the values to be
|
|
149
|
+
grouped.
|
|
150
|
+
|
|
151
|
+
:rtype: dict
|
|
152
|
+
:returns: A dictionary where keys are field values and values are
|
|
153
|
+
dictionaries containing a label and an "entries" list.
|
|
154
|
+
"""
|
|
155
|
+
grouped_values = {}
|
|
156
|
+
|
|
157
|
+
for entry in values:
|
|
158
|
+
result = get_value_and_label_for_field(fields, entry, field_name)
|
|
159
|
+
value, label = result['value'], result['label']
|
|
160
|
+
|
|
161
|
+
if value not in grouped_values:
|
|
162
|
+
grouped_values[value] = {'label': label, 'entries': []}
|
|
163
|
+
|
|
164
|
+
grouped_values[value]['entries'].append(entry)
|
|
165
|
+
|
|
166
|
+
return grouped_values
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_all_objects_in_grouped_values(grouped):
|
|
170
|
+
"""
|
|
171
|
+
Retrieve all objects within grouped values.
|
|
172
|
+
|
|
173
|
+
:param dict grouped: A dictionary where keys are group identifiers and
|
|
174
|
+
values are dictionaries with a key "entries" containing lists of
|
|
175
|
+
objects.
|
|
176
|
+
|
|
177
|
+
:rtype: list
|
|
178
|
+
:returns: A list containing all objects from each group's "entries".
|
|
179
|
+
"""
|
|
180
|
+
total_objects = []
|
|
181
|
+
|
|
182
|
+
for key in grouped:
|
|
183
|
+
group = grouped[key]
|
|
184
|
+
total_objects.extend(group['entries'])
|
|
185
|
+
|
|
186
|
+
return total_objects
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
from dateutil.relativedelta import relativedelta
|
|
3
|
+
|
|
4
|
+
from ooui.graph.fields import get_value_for_operator
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def process_timerange_data(values, timerange, interval=1):
|
|
8
|
+
"""
|
|
9
|
+
Process time range data by combining values and filling gaps.
|
|
10
|
+
|
|
11
|
+
:param list values: A list de diccionaris representing the original data.
|
|
12
|
+
:param str timerange: The time range unit ("day", "week", "month", "year").
|
|
13
|
+
:param int interval: The interval to increment dates by.
|
|
14
|
+
|
|
15
|
+
:rtype: list
|
|
16
|
+
:returns: A list containing the processed values with gaps filled.
|
|
17
|
+
"""
|
|
18
|
+
combined_values = combine_values_for_timerange(values, timerange)
|
|
19
|
+
filled_values = fill_gaps_in_timerange_data(
|
|
20
|
+
combined_values, timerange, interval
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
return filled_values
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def fill_gaps_in_timerange_data(values, timerange, interval):
|
|
27
|
+
"""
|
|
28
|
+
Fill gaps in time range data by inserting missing dates.
|
|
29
|
+
|
|
30
|
+
:param list values: A list of dictionaries representing the original data.
|
|
31
|
+
:param str timerange: The time range unit ("day", "week", "month", "year").
|
|
32
|
+
:param int interval: The interval to increment dates by.
|
|
33
|
+
|
|
34
|
+
:rtype: list
|
|
35
|
+
:returns: A new list containing all values with gaps filled in.
|
|
36
|
+
"""
|
|
37
|
+
final_values = []
|
|
38
|
+
unique_values = get_unique_values_grouped_by(values, 'type-stacked')
|
|
39
|
+
|
|
40
|
+
for key, values_for_key in unique_values.items():
|
|
41
|
+
for i in range(len(values_for_key)):
|
|
42
|
+
value = values_for_key[i]
|
|
43
|
+
final_values.append(value)
|
|
44
|
+
|
|
45
|
+
if i == len(values_for_key) - 1:
|
|
46
|
+
break
|
|
47
|
+
|
|
48
|
+
date = value['x']
|
|
49
|
+
next_date = values_for_key[i + 1]['x']
|
|
50
|
+
|
|
51
|
+
missing_dates = get_missing_consecutive_dates(
|
|
52
|
+
[date, next_date], timerange, interval
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
final_values.extend([
|
|
56
|
+
{
|
|
57
|
+
'x': string_date,
|
|
58
|
+
'value': 0,
|
|
59
|
+
'type': value['type'],
|
|
60
|
+
'stacked': value['stacked']
|
|
61
|
+
} for string_date in missing_dates
|
|
62
|
+
])
|
|
63
|
+
|
|
64
|
+
return final_values
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def add_time_unit(start_date, interval, units):
|
|
68
|
+
if units == 'days':
|
|
69
|
+
return start_date + timedelta(days=interval)
|
|
70
|
+
elif units == 'weeks':
|
|
71
|
+
return start_date + timedelta(weeks=interval)
|
|
72
|
+
elif units == 'months':
|
|
73
|
+
return start_date + relativedelta(months=interval)
|
|
74
|
+
elif units == 'years':
|
|
75
|
+
return start_date + relativedelta(years=interval)
|
|
76
|
+
else:
|
|
77
|
+
raise ValueError("Unsupported units: {}".format(units))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_missing_consecutive_dates(dates, timerange, interval=1):
|
|
81
|
+
"""
|
|
82
|
+
Retrieve the missing consecutive dates within a range according to a specific time unit.
|
|
83
|
+
|
|
84
|
+
:param list dates: A list of strings representing the sorted original dates.
|
|
85
|
+
:param str timerange: The time unit for checking consecutive dates ("day", "week", "month", "year").
|
|
86
|
+
:param int interval: An optional interval to increment dates by.
|
|
87
|
+
|
|
88
|
+
:rtype: list
|
|
89
|
+
:returns: A list of missing dates as strings.
|
|
90
|
+
"""
|
|
91
|
+
missing_dates = []
|
|
92
|
+
units = "{}s".format(timerange)
|
|
93
|
+
format_str = get_format_for_units(units)
|
|
94
|
+
|
|
95
|
+
if len(dates) == 1:
|
|
96
|
+
return dates
|
|
97
|
+
|
|
98
|
+
sorted_dates = sorted(dates)
|
|
99
|
+
|
|
100
|
+
for i in range(len(sorted_dates) - 1):
|
|
101
|
+
date1 = datetime.strptime(sorted_dates[i], format_str)
|
|
102
|
+
date2 = datetime.strptime(sorted_dates[i + 1], format_str)
|
|
103
|
+
|
|
104
|
+
next_date = add_time_unit(date1, interval, units)
|
|
105
|
+
|
|
106
|
+
while next_date < date2:
|
|
107
|
+
missing_dates.append(next_date.strftime(format_str))
|
|
108
|
+
next_date = add_time_unit(next_date, interval, units)
|
|
109
|
+
|
|
110
|
+
return missing_dates
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def combine_values_for_timerange(values, timerange):
|
|
114
|
+
"""
|
|
115
|
+
Combine values according to a specific time range.
|
|
116
|
+
|
|
117
|
+
:param list values: A list of dictionaries representing the original data.
|
|
118
|
+
:param str timerange: The time range for adjusting and combining the values.
|
|
119
|
+
|
|
120
|
+
:rtype: list
|
|
121
|
+
:returns: A list of dictionaries containing the final combined values.
|
|
122
|
+
"""
|
|
123
|
+
adjusted_values = adjust_x_values_for_time_range(values, timerange)
|
|
124
|
+
unique_values = get_unique_values_grouped_by(adjusted_values, 'all')
|
|
125
|
+
|
|
126
|
+
final_values = []
|
|
127
|
+
|
|
128
|
+
for key, values_for_key in unique_values.items():
|
|
129
|
+
operator = values_for_key[0]['operator']
|
|
130
|
+
if operator == 'count':
|
|
131
|
+
operator = '+'
|
|
132
|
+
|
|
133
|
+
final_value = get_value_for_operator(
|
|
134
|
+
operator, [entry['value'] for entry in values_for_key]
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
final_values.append(dict(values_for_key[0], value=final_value))
|
|
138
|
+
|
|
139
|
+
return final_values
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def adjust_x_values_for_time_range(values, timerange):
|
|
143
|
+
"""
|
|
144
|
+
Adjust the "x" values in a list of items according to the specified
|
|
145
|
+
time range.
|
|
146
|
+
|
|
147
|
+
:param list values: A list of dictionaries containing the original data.
|
|
148
|
+
:param str timerange: The time range for adjusting the "x" values.
|
|
149
|
+
|
|
150
|
+
:rtype: list
|
|
151
|
+
:returns: A new list where each item's "x" property is adjusted based on
|
|
152
|
+
the time range.
|
|
153
|
+
"""
|
|
154
|
+
return [
|
|
155
|
+
dict(value, x=convert_date_to_time_range_adjusted(value['x'], timerange))
|
|
156
|
+
for value in values
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def convert_date_to_time_range_adjusted(date, timerange):
|
|
161
|
+
"""
|
|
162
|
+
Adjust a date to a specific time range format.
|
|
163
|
+
|
|
164
|
+
:param str date: The original date string.
|
|
165
|
+
:param str timerange: The time range for adjustment
|
|
166
|
+
("hour", "day", "week", "month", "year").
|
|
167
|
+
|
|
168
|
+
:rtype: str
|
|
169
|
+
:returns: The adjusted date in the specified time range format.
|
|
170
|
+
"""
|
|
171
|
+
format_str = get_date_format(date)
|
|
172
|
+
moment_date = datetime.strptime(date, format_str)
|
|
173
|
+
|
|
174
|
+
if timerange == 'hour':
|
|
175
|
+
return moment_date.strftime('%Y-%m-%d %H:00')
|
|
176
|
+
elif timerange == 'day':
|
|
177
|
+
return moment_date.strftime('%Y-%m-%d')
|
|
178
|
+
elif timerange == 'week':
|
|
179
|
+
return moment_date.strftime('%Y-%W')
|
|
180
|
+
elif timerange == 'month':
|
|
181
|
+
return moment_date.strftime('%Y-%m')
|
|
182
|
+
elif timerange == 'year':
|
|
183
|
+
return moment_date.strftime('%Y')
|
|
184
|
+
else:
|
|
185
|
+
raise ValueError("Unsupported timerange: {}".format(timerange))
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def get_date_format(date_str):
|
|
189
|
+
"""
|
|
190
|
+
Determine the appropriate date format string based on whether the input
|
|
191
|
+
string contains a colon.
|
|
192
|
+
|
|
193
|
+
:param str date_str: A string representing the date to be checked.
|
|
194
|
+
|
|
195
|
+
:rtype: str
|
|
196
|
+
:returns: The appropriate date format string compatible with
|
|
197
|
+
`datetime.strptime`.
|
|
198
|
+
"""
|
|
199
|
+
if ':' in date_str:
|
|
200
|
+
return '%Y-%m-%d %H:%M:%S'
|
|
201
|
+
else:
|
|
202
|
+
return '%Y-%m-%d'
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def get_unique_values_grouped_by(values, group_by):
|
|
206
|
+
"""
|
|
207
|
+
Group values by unique keys determined by the grouping criteria.
|
|
208
|
+
|
|
209
|
+
:param list values: A list of dictionaries representing the values.
|
|
210
|
+
:param str group_by: Grouping criteria. Can be "all" or "type-stacked".
|
|
211
|
+
|
|
212
|
+
:rtype: dict
|
|
213
|
+
:returns: A dictionary where keys are unique identifiers based on the
|
|
214
|
+
grouping criteria, and values are lists of grouped items.
|
|
215
|
+
"""
|
|
216
|
+
unique_values = {}
|
|
217
|
+
|
|
218
|
+
for value in values:
|
|
219
|
+
x = value.get('x')
|
|
220
|
+
value_type = value.get('type')
|
|
221
|
+
stacked = value.get('stacked')
|
|
222
|
+
|
|
223
|
+
if group_by == 'all':
|
|
224
|
+
unique_key = "{}-{}-{}".format(x, value_type, stacked)
|
|
225
|
+
else:
|
|
226
|
+
unique_key = "{}-{}".format(value_type, stacked)
|
|
227
|
+
|
|
228
|
+
if unique_key not in unique_values:
|
|
229
|
+
unique_values[unique_key] = []
|
|
230
|
+
|
|
231
|
+
unique_values[unique_key].append(value)
|
|
232
|
+
|
|
233
|
+
return unique_values
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def get_format_for_units(units):
|
|
237
|
+
"""
|
|
238
|
+
Retrieve the appropriate date format string based on the units provided.
|
|
239
|
+
|
|
240
|
+
:param str units: A string representing the time units
|
|
241
|
+
(e.g., "days", "weeks", "months", "years", or "hours").
|
|
242
|
+
|
|
243
|
+
:rtype: str
|
|
244
|
+
:returns: The appropriate date format string based on the provided units.
|
|
245
|
+
"""
|
|
246
|
+
if units == 'days':
|
|
247
|
+
return '%Y-%m-%d'
|
|
248
|
+
elif units == 'weeks':
|
|
249
|
+
return '%Y-%W'
|
|
250
|
+
elif units == 'months':
|
|
251
|
+
return '%Y-%m'
|
|
252
|
+
elif units == 'years':
|
|
253
|
+
return '%Y'
|
|
254
|
+
else:
|
|
255
|
+
return '%Y-%m-%d %H:%M'
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def check_dates_consecutive(dates, unit):
|
|
259
|
+
"""
|
|
260
|
+
Check if the dates in the list are consecutive based on the provided unit.
|
|
261
|
+
|
|
262
|
+
:param list dates: A list of strings representing the dates to be checked.
|
|
263
|
+
:param str unit: The unit to check for consecutiveness ("hours", "days", "weeks", "months", or "years").
|
|
264
|
+
|
|
265
|
+
:rtype: bool
|
|
266
|
+
:returns: True if the dates are consecutive in the given unit, otherwise False.
|
|
267
|
+
"""
|
|
268
|
+
if len(dates) == 0:
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
if len(dates) == 1:
|
|
272
|
+
return True
|
|
273
|
+
|
|
274
|
+
consecutive = False
|
|
275
|
+
format_str = get_format_for_units(unit)
|
|
276
|
+
|
|
277
|
+
for i in range(len(dates) - 1):
|
|
278
|
+
date1 = datetime.strptime(dates[i], format_str)
|
|
279
|
+
date2 = datetime.strptime(dates[i + 1], format_str)
|
|
280
|
+
|
|
281
|
+
if unit == 'hours':
|
|
282
|
+
diff = (date2 - date1).total_seconds() / 3600
|
|
283
|
+
elif unit == 'days':
|
|
284
|
+
diff = (date2 - date1).days
|
|
285
|
+
elif unit == 'weeks':
|
|
286
|
+
diff = (date2 - date1).days // 7
|
|
287
|
+
elif unit == 'months':
|
|
288
|
+
diff = relativedelta(date2, date1).months + (relativedelta(date2, date1).years * 12)
|
|
289
|
+
elif unit == 'years':
|
|
290
|
+
diff = relativedelta(date2, date1).years
|
|
291
|
+
else:
|
|
292
|
+
raise ValueError("Unsupported unit: {}".format(unit))
|
|
293
|
+
|
|
294
|
+
if abs(diff) == 1:
|
|
295
|
+
consecutive = True
|
|
296
|
+
else:
|
|
297
|
+
consecutive = False
|
|
298
|
+
break
|
|
299
|
+
|
|
300
|
+
return consecutive
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import six
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def parse_bool_attribute(attribute):
|
|
5
|
+
"""
|
|
6
|
+
Parse a boolean attribute.
|
|
7
|
+
:param attribute:
|
|
8
|
+
:return: bool
|
|
9
|
+
"""
|
|
10
|
+
return str(attribute).lower() in ("1", "true")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def replace_entities(text):
|
|
14
|
+
"""
|
|
15
|
+
Replace all HTML entities with their respective unicode characters.
|
|
16
|
+
:param text:
|
|
17
|
+
:return:
|
|
18
|
+
"""
|
|
19
|
+
if six.PY2:
|
|
20
|
+
from HTMLParser import HTMLParser
|
|
21
|
+
parser = HTMLParser()
|
|
22
|
+
return parser.unescape(text)
|
|
23
|
+
else:
|
|
24
|
+
import html
|
|
25
|
+
return html.unescape(text)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ooui
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Open Object User Interface
|
|
5
|
+
Home-page: https://github.com/gisce/python-ooui
|
|
6
|
+
Author: GISCE
|
|
7
|
+
Author-email: devel@gisce.net
|
|
8
|
+
License: MIT
|
|
9
|
+
Provides: ooui
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
|
|
12
|
+
Open Object User Interface for GISCE-ERP
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
ooui/__init__.py
|
|
5
|
+
ooui.egg-info/PKG-INFO
|
|
6
|
+
ooui.egg-info/SOURCES.txt
|
|
7
|
+
ooui.egg-info/dependency_links.txt
|
|
8
|
+
ooui.egg-info/requires.txt
|
|
9
|
+
ooui.egg-info/top_level.txt
|
|
10
|
+
ooui/graph/__init__.py
|
|
11
|
+
ooui/graph/axis.py
|
|
12
|
+
ooui/graph/base.py
|
|
13
|
+
ooui/graph/chart.py
|
|
14
|
+
ooui/graph/fields.py
|
|
15
|
+
ooui/graph/indicator.py
|
|
16
|
+
ooui/graph/processor.py
|
|
17
|
+
ooui/graph/timerange.py
|
|
18
|
+
ooui/helpers/__init__.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ooui
|
ooui-0.1.0/setup.cfg
ADDED
ooui-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name='ooui',
|
|
5
|
+
description='Open Object User Interface',
|
|
6
|
+
author='GISCE',
|
|
7
|
+
author_email='devel@gisce.net',
|
|
8
|
+
url='https://github.com/gisce/python-ooui',
|
|
9
|
+
version='0.1.0',
|
|
10
|
+
license='MIT',
|
|
11
|
+
long_description='''Open Object User Interface for GISCE-ERP''',
|
|
12
|
+
provides=['ooui'],
|
|
13
|
+
install_requires=[
|
|
14
|
+
'lxml',
|
|
15
|
+
'python-dateutil',
|
|
16
|
+
'six',
|
|
17
|
+
],
|
|
18
|
+
tests_require=[
|
|
19
|
+
'mamba',
|
|
20
|
+
'expects',
|
|
21
|
+
],
|
|
22
|
+
packages=find_packages()
|
|
23
|
+
)
|