Commit 4cd5de89 authored by LE GAC Renaud's avatar LE GAC Renaud
Browse files

Refactor List. It now relies on DataFrame as the others reporting tools and deal with year axis.

parent 6c9d1fdb
......@@ -6,6 +6,7 @@
- the content of the report is defined in the database
"""
import json
import re
from plugin_dbui import INLINE_ALERT
......@@ -69,7 +70,7 @@ def list():
report = List(config, selector)
store = report.to_store()
grid = report.to_grid()
# delegate the grid rendering to the view
response.view = 'report/grid.html'
return dict(cfg_store=json.dumps(store), view=grid)
......@@ -96,10 +97,6 @@ def metric1D():
store = report.to_store()
grid = report.to_grid()
# JSON encoding
grid.columns = json.dumps(grid.columns)
grid.features = json.dumps(grid.features)
# delegate to standard view
response.view = 'report/grid.html'
return dict(cfg_store=json.dumps(store), view=grid)
......@@ -130,10 +127,6 @@ def metric2D():
report = Metric2D(config, selector)
store = report.to_store()
grid = report.to_grid()
# JSON encoding
grid.columns = json.dumps(grid.columns)
grid.features = json.dumps(grid.features)
# delegate the rendering to the standard view
response.view = 'report/grid.html'
......
......@@ -25,10 +25,23 @@ T("Any field of the history table including foreign table. "
"For properties in the the user data block, use history.data.myproperty.")
def_columns = \
"[{xtype: 'rownumberer'},\n{text: '', dataIndex: '', flex:1}]"
"""[{
"xtype": "rownumberer"
}, {
"dbfield": "?.?",
"flex": 1,
"text": "?",
"xtype": "gridcolumn"
}]"""
def_features = \
"[{ftype:'grouping', groupHeaderTpl: '{name}', startCollapsed: false}]"
"""[{
"ftype": "grouping",
"groupHeaderTpl": "{name}",
"startCollapsed": false
}]"""
if MIGRATE:
def_columns = None
......
......@@ -17,11 +17,11 @@ T("Any field of the history table including those of the foreign tables. "
def_columns = \
"""[{
"aggregate": "",
"align": "right",
"dbfield": "",
"format": "0.0",
"text": ""
"aggregate": "?",
"align": "right",
"dbfield": "?.?",
"format": "0.0",
"text": "?"
}]"""
......
......@@ -13,7 +13,7 @@ from gluon.storage import Storage
from plugin_dbui import Store
def get_value(row, tablename, fieldname, keyname=''):
def get_value(row, tablename, fieldname, keyname='', **kwargs):
"""Return the row value of the database field
identify by the tablename, fieldname and keyname
......@@ -30,17 +30,22 @@ def get_value(row, tablename, fieldname, keyname=''):
@param keyname:
@return: either C{row[tablename][fieldname]} or
C{row[tablename][fieldname][keyname]} for JSON type field.
C{row[tablename][fieldname][keyname]} for JSON type field or
C{kwargs[tablename]} when fieldname and keyname are not defined
"""
# standard field
if not keyname:
if fieldname and not keyname:
return row[tablename][fieldname]
# JSON type database field containing a dict
if keyname in row[tablename][fieldname]:
if keyname and keyname in row[tablename][fieldname]:
return row[tablename][fieldname][keyname]
# when only tablename is defined have a look to the keyword value
if tablename in kwargs:
return kwargs[tablename]
return ""
......@@ -91,19 +96,14 @@ class BaseReport(object):
@ivar selector: the selector containing user criteria
"""
def __init__(self, config, selector, select=True):
def __init__(self, config, selector):
"""
@type config: gluon.dal.Row
@param config: the list configuration parameter
@type selector: MySelector
@param selector: the selector handling user criteria
@type select: Boolean
@param select: the history records matching user criteria
are selected and stored in the C{rows} attributes when true.
The default is C{True}.
"""
db = current.globalenv['db']
......@@ -117,12 +117,26 @@ class BaseReport(object):
if config.conditions:
q_conditions = smart_query(db.history, config.conditions)
selector.append_query(q_conditions)
def _get_years(self):
"""build the range of years selected by the user.
"""
start = self.selector.period_start.year
end = self.selector.period_end.year
years = range(start, end + 1)
return years
# retrieve the history records from the database
# it contains all fields of the history and foreign tables
# as well as the virtual field register by the selector
if select:
self.rows = selector.select(db.history)
def to_df(self):
"""Return as pandas DataFrame.
@rtype: pandas.DataFrame
"""
return self.df
class List(BaseReport):
......@@ -140,33 +154,117 @@ class List(BaseReport):
Its configuration is returned by the method L{to_store}.
"""
def _set_store_data(self):
""" Generate the C{Ext.data.Store.data} property.
It is a list of dictionaries. Each of them contains the data
for one row. One key, value pair for each C{Ext.data.Field} where
the key is the name of the C{Ext.data.Field}.
def __init__(self, config, selector):
BaseReport.__init__(self, config, selector)
# database field maps and address
# NOTE: not defined for "rownumber" column
columns = [Storage(el) for el in json.loads(config.columns)]
maps = [split_dbfield(el.dbfield) for el in columns if el.dbfield]
# columns and maps are persistent
self._columns = columns
self._maps = maps
# instantiate and fill the DataFrame
self._do_metric()
def _do_data(self):
"""Build a temporarily list with the raw data for each series.
This method handle the "year" database field.
@rtype: list
"""
map = self._map
data = []
for row in self.rows:
config = self.config
db = self.db
maps = self._maps
selector = self.selector
is_year = [True for el in maps if el[0] == 'year']
if is_year:
# build the year range
years = self._get_years()
if config.conditions:
q_conditions = selector._extra_queries[0]
for year in years:
selector.reset_extra_queries()
selector.period_start = date(year, 1, 1)
selector.period_end = date(year, 12, 31)
di = dict()
for field in self._store.fields:
value = get_value(row, *map[field.name])
if config.conditions:
selector.append_query(q_conditions)
# encode date
if isinstance(value, date):
value = value.strftime("%Y-%m-%d")
for row in selector.select(db.history):
li = []
# encode time delta
elif isinstance(value, timedelta):
value = value.total_seconds()
di[field.name] = value
for map in maps:
value = get_value(row, *map, year=year)
value = self._encode(value)
li.append(value)
self._store.data.append(di)
data.append(li)
else:
for row in selector.select(db.history):
li = []
for map in maps:
value = get_value(row, *map)
value = self._encode(value)
li.append(value)
data.append(li)
return data
def _do_metric(self):
"""Interface the database with the DataFrame structure.
This method handle the "year" database field.
"""
dataIndex = [''.join(el) for el in self._maps]
data = self._do_data()
df = pd.DataFrame(data, columns=dataIndex)
self.df = df
def _encode(self, value):
"""Encode properly date and timedelta.
"""
# encode date
if isinstance(value, date):
value = value.strftime("%Y-%m-%d")
# encode time delta
elif isinstance(value, timedelta):
value = value.total_seconds()
return value
def _set_store_data(self):
""" Generate the C{Ext.data.Store.data} property.
It is a list of dictionaries. Each of them contains the data
for one row. One key, value pair for each C{Ext.data.Field} where
the key is the name of the C{Ext.data.Field}.
"""
store = self._store
for row in self.df.T.to_dict().itervalues():
store.data.append(row)
def _set_store_fields(self):
......@@ -180,28 +278,30 @@ class List(BaseReport):
"""
db = self.db
config = self.config
map = self._map
# extract the address of database fields from the list configuration
addresses = re.findall("dataIndex *: *'([a-z_\.]+)'", config.columns)
maps = self._maps
store = self._store
# convert the database field into the configuration of an Ext.data.Field
for address in addresses:
# get the database field link to the column.
# keep in mind that the address is encoded ad
# table.field or table.field.key
tablename, fieldname, keyname = split_dbfield(address)
for map in maps:
cfg = Storage()
tablename, fieldname, keyname = map
# special case the year
if tablename == 'year':
cfg.name = 'year'
cfg.type ='int'
store.fields.append(cfg)
continue
dbfield = db[tablename][fieldname]
# keep a map between the column in the store and the
# database field for a later use when retrieving the data.
name = address.replace('.', '')
map[name] = (tablename, fieldname, keyname)
# the store index is derived from the dbfield name
# it will be used by the grid via the dataIndex property
# The name can not contain dot.
cfg.name = ''.join(map)
# build the configuration for the Ext.data.Field
cfg = Storage(name=name)
cfg.type = dbfield.type
if dbfield.type in ('string', 'text', 'json'):
......@@ -225,30 +325,41 @@ class List(BaseReport):
cfg.type = 'date'
cfg.dateFormat = 'H:i:s'
self._store.fields.append(cfg)
store.fields.append(cfg)
def to_grid(self):
"""Build the configuration of the C{App.grid.Panel}.
The configuration of the list is provided as input of the constructor.
It contains the C{dataIndex} property which is the address of the
database field displayed in a column.
It is replaced by the index of the data in the C{Ext.data.Store}.
@rtype: plugin_dbui.Grid
@return: the configuration of the C{App.grid.Panel}.
The properties columns and features are JSON encoded.
"""
config = self.config
# encode the Ext.grid.column.dataIndex to match the Ext.data.Field name
s = config.columns.replace("\n", "").replace(" ", "")
s = re.sub("dataIndex:'(\w+)\.(\w+)'", "dataIndex:'\\1\\2'", s)
s = re.sub("dataIndex:'(\w+)\.(\w+)\.(\w+)'", "dataIndex:'\\1\\2\\3'", s)
config.columns = s
return config
grid = Storage(columns=[], features=[], title=config.title)
# column from the configuration
for cfg in self._columns:
# encode the dataIndex
# not needed for rownumber column
if cfg.dbfield:
cfg.dataIndex = cfg.dbfield.replace('.', '')
del cfg.dbfield
grid.columns.append(cfg)
# features from the configuration
grid.features = config.features
# JSON encoding
# reference to javascript function or the function itself
# are surrounded by double quoted. removed them.
grid.columns = json.dumps(grid.columns)
grid.columns = re.sub('("renderer": *)(")(\w+)(")', r'\1\3',grid.columns)
return grid
def to_store(self):
......@@ -301,7 +412,7 @@ class Metric1D(BaseReport):
"""
def __init__(self, config, selector):
BaseReport.__init__(self, config, selector, select=False)
BaseReport.__init__(self, config, selector)
map = split_dbfield(config.group_field)
......@@ -442,6 +553,7 @@ class Metric1D(BaseReport):
@rtype: plugin_dbui.Grid
@return: the configuration of the C{App.grid.Panel}.
The columns and features property are JSON encoded.
"""
config = self.config
......@@ -468,6 +580,10 @@ class Metric1D(BaseReport):
# activate summary feature
grid.features = [{'ftype': 'summary'}]
# JSON encoding
grid.columns = json.dumps(grid.columns)
grid.features = json.dumps(grid.features)
return grid
......@@ -501,7 +617,7 @@ class Metric2D(BaseReport):
"""
def __init__(self, config, selector):
BaseReport.__init__(self, config, selector, select=False)
BaseReport.__init__(self, config, selector)
self._do_metric()
# replace undefined value by 0
......@@ -655,6 +771,7 @@ class Metric2D(BaseReport):
@rtype: plugin_dbui.Grid
@return: the configuration of the C{App.grid.Panel}.
The columns and features properties are JSON encoded.
"""
config = self.config
......@@ -678,7 +795,11 @@ class Metric2D(BaseReport):
'xtype': 'numbercolumn'})
grid.features.append({'ftype': 'summary'})
# JSON encoding
grid.columns = json.dumps(grid.columns)
grid.features = json.dumps(grid.features)
return grid
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment