views.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. import logging
  2. from flask_babel import lazy_gettext
  3. from .jsontools import dict_to_json
  4. from .widgets import ChartWidget, DirectChartWidget
  5. from ..baseviews import BaseModelView, expose
  6. from ..models.group import DirectProcessData, GroupByProcessData
  7. from ..security.decorators import has_access
  8. from ..urltools import get_filter_args
  9. from ..widgets import SearchWidget
  10. log = logging.getLogger(__name__)
  11. class BaseChartView(BaseModelView):
  12. """
  13. This is the base class for all chart views.
  14. Use DirectByChartView or GroupByChartView, override their properties
  15. and their base classes
  16. (BaseView, BaseModelView, BaseChartView) to customise your charts
  17. """
  18. chart_template = "appbuilder/general/charts/chart.html"
  19. """ The chart template, override to implement your own """
  20. chart_widget = ChartWidget
  21. """ Chart widget override to implement your own """
  22. search_widget = SearchWidget
  23. """ Search widget override to implement your own """
  24. chart_title = "Chart"
  25. """ A title to be displayed on the chart """
  26. title = "Title"
  27. group_by_label = lazy_gettext("Group by")
  28. """ The label that is displayed for the chart selection """
  29. default_view = "chart"
  30. chart_type = "PieChart"
  31. """ The chart type PieChart, ColumnChart, LineChart """
  32. chart_3d = "true"
  33. """ Will display in 3D? """
  34. width = 400
  35. """ The width """
  36. height = "400px"
  37. group_bys = {}
  38. """ New for 0.6.4, on test, don't use yet """
  39. def __init__(self, **kwargs):
  40. self._init_titles()
  41. super(BaseChartView, self).__init__(**kwargs)
  42. def _init_titles(self):
  43. self.title = self.chart_title
  44. def _get_chart_widget(self, filters=None, widgets=None, **args):
  45. raise NotImplementedError
  46. def _get_view_widget(self, **kwargs):
  47. """
  48. :return:
  49. Returns a widget
  50. """
  51. return self._get_chart_widget(**kwargs).get("chart")
  52. class GroupByChartView(BaseChartView):
  53. definitions = []
  54. """
  55. These charts can display multiple series,
  56. based on columns or methods defined on models.
  57. You can display multiple charts on the same view.
  58. This data can be grouped and aggregated has you like.
  59. :label: (optional) String label to display on chart selection.
  60. :group: String with the column name or method from model.
  61. :formatter: (optional) function that formats the output of 'group' key
  62. :series: A list of tuples with the aggregation function and the column name
  63. to apply the aggregation
  64. ::
  65. [{
  66. 'label': 'String',
  67. 'group': '<COLNAME>'|'<FUNCNAME>'
  68. 'formatter: <FUNC>
  69. 'series': [(<AGGR FUNC>, <COLNAME>|'<FUNCNAME>'),...]
  70. }
  71. ]
  72. example::
  73. class CountryGroupByChartView(GroupByChartView):
  74. datamodel = SQLAInterface(CountryStats)
  75. chart_title = 'Statistics'
  76. definitions = [
  77. {
  78. 'label': 'Country Stat',
  79. 'group': 'country',
  80. 'series': [(aggregate_avg, 'unemployed_perc'),
  81. (aggregate_avg, 'population'),
  82. (aggregate_avg, 'college_perc')
  83. ]
  84. }
  85. ]
  86. """
  87. chart_type = "ColumnChart"
  88. chart_template = "appbuilder/general/charts/jsonchart.html"
  89. chart_widget = DirectChartWidget
  90. ProcessClass = GroupByProcessData
  91. def __init__(self, **kwargs):
  92. super(GroupByChartView, self).__init__(**kwargs)
  93. for definition in self.definitions:
  94. col = definition.get("group")
  95. # Setup labels
  96. try:
  97. self.label_columns[col] = (
  98. definition.get("label") or self.label_columns[col]
  99. )
  100. except Exception:
  101. self.label_columns[col] = self._prettify_column(col)
  102. if not definition.get("label"):
  103. definition["label"] = self.label_columns[col]
  104. # Setup Series
  105. for serie in definition["series"]:
  106. if isinstance(serie, tuple):
  107. if hasattr(serie[0], "_label"):
  108. key = serie[0].__name__ + serie[1]
  109. self.label_columns[key] = (
  110. serie[0]._label + " " + self._prettify_column(serie[1])
  111. )
  112. else:
  113. self.label_columns[serie] = self._prettify_column(serie)
  114. def get_group_by_class(self, definition):
  115. """
  116. intantiates the processing class (Direct or Grouped) and returns it.
  117. """
  118. group_by = definition["group"]
  119. series = definition["series"]
  120. if "formatter" in definition:
  121. formatter = {group_by: definition["formatter"]}
  122. else:
  123. formatter = {}
  124. return self.ProcessClass([group_by], series, formatter)
  125. def _get_chart_widget(
  126. self,
  127. filters=None,
  128. order_column="",
  129. order_direction="",
  130. widgets=None,
  131. direct=None,
  132. height=None,
  133. definition="",
  134. **args
  135. ):
  136. height = height or self.height
  137. widgets = widgets or dict()
  138. joined_filters = filters.get_joined_filters(self._base_filters)
  139. # check if order_column may be database ordered
  140. if not self.datamodel.get_order_columns_list([order_column]):
  141. order_column = ""
  142. order_direction = ""
  143. count, lst = self.datamodel.query(
  144. filters=joined_filters,
  145. order_column=order_column,
  146. order_direction=order_direction,
  147. )
  148. if not definition:
  149. definition = self.definitions[0]
  150. group = self.get_group_by_class(definition)
  151. value_columns = group.to_json(
  152. group.apply(lst, sort=order_column == ""), self.label_columns
  153. )
  154. widgets["chart"] = self.chart_widget(
  155. route_base=self.route_base,
  156. chart_title=self.chart_title,
  157. chart_type=self.chart_type,
  158. chart_3d=self.chart_3d,
  159. height=height,
  160. value_columns=value_columns,
  161. modelview_name=self.__class__.__name__,
  162. **args
  163. )
  164. return widgets
  165. @expose("/chart/<group_by>")
  166. @expose("/chart/")
  167. @has_access
  168. def chart(self, group_by=0):
  169. group_by = int(group_by)
  170. form = self.search_form.refresh()
  171. get_filter_args(self._filters)
  172. widgets = self._get_chart_widget(
  173. filters=self._filters,
  174. definition=self.definitions[group_by],
  175. order_column=self.definitions[group_by]["group"],
  176. order_direction="asc",
  177. )
  178. widgets = self._get_search_widget(form=form, widgets=widgets)
  179. self.update_redirect()
  180. return self.render_template(
  181. self.chart_template,
  182. route_base=self.route_base,
  183. title=self.chart_title,
  184. label_columns=self.label_columns,
  185. definitions=self.definitions,
  186. group_by_label=self.group_by_label,
  187. height=self.height,
  188. widgets=widgets,
  189. appbuilder=self.appbuilder,
  190. )
  191. class DirectByChartView(GroupByChartView):
  192. """
  193. Use this class to display charts with multiple series,
  194. based on columns or methods defined on models.
  195. You can display multiple charts on the same view.
  196. Default routing point is '/chart'
  197. Setup definitions property to configure the chart
  198. :label: (optional) String label to display on chart selection.
  199. :group: String with the column name or method from model.
  200. :formatter: (optional) function that formats the output of 'group' key
  201. :series: A list of tuples with the aggregation function and the column name
  202. to apply the aggregation
  203. The **definitions** property respects the following grammar::
  204. definitions = [
  205. {
  206. 'label': 'label for chart definition',
  207. 'group': '<COLNAME>'|'<MODEL FUNCNAME>',
  208. 'formatter': <FUNC FORMATTER FOR GROUP COL>,
  209. 'series': ['<COLNAME>'|'<MODEL FUNCNAME>',...]
  210. }, ...
  211. ]
  212. example::
  213. class CountryDirectChartView(DirectByChartView):
  214. datamodel = SQLAInterface(CountryStats)
  215. chart_title = 'Direct Data Example'
  216. definitions = [
  217. {
  218. 'label': 'Unemployment',
  219. 'group': 'stat_date',
  220. 'series': ['unemployed_perc',
  221. 'college_perc']
  222. }
  223. ]
  224. """
  225. ProcessClass = DirectProcessData
  226. # -------------------------------------------------------
  227. # DEPRECATED SECTION
  228. # -------------------------------------------------------
  229. class BaseSimpleGroupByChartView(BaseChartView): # pragma: no cover
  230. group_by_columns = []
  231. """ A list of columns to be possibly grouped by, this list must be filled """
  232. def __init__(self, **kwargs):
  233. if not self.group_by_columns:
  234. raise Exception(
  235. "Base Chart View property <group_by_columns> must not be empty"
  236. )
  237. else:
  238. super(BaseSimpleGroupByChartView, self).__init__(**kwargs)
  239. def _get_chart_widget(
  240. self,
  241. filters=None,
  242. order_column="",
  243. order_direction="",
  244. widgets=None,
  245. group_by=None,
  246. height=None,
  247. **args
  248. ):
  249. height = height or self.height
  250. widgets = widgets or dict()
  251. group_by = group_by or self.group_by_columns[0]
  252. joined_filters = filters.get_joined_filters(self._base_filters)
  253. value_columns = self.datamodel.query_simple_group(
  254. group_by, filters=joined_filters
  255. )
  256. widgets["chart"] = self.chart_widget(
  257. route_base=self.route_base,
  258. chart_title=self.chart_title,
  259. chart_type=self.chart_type,
  260. chart_3d=self.chart_3d,
  261. height=height,
  262. value_columns=value_columns,
  263. modelview_name=self.__class__.__name__,
  264. **args
  265. )
  266. return widgets
  267. class BaseSimpleDirectChartView(BaseChartView): # pragma: no cover
  268. direct_columns = []
  269. """
  270. Make chart using the column on the dict
  271. chart_columns = {'chart label 1':('X column','Y1 Column','Y2 Column, ...),
  272. 'chart label 2': ('X Column','Y1 Column',...),...}
  273. """
  274. def __init__(self, **kwargs):
  275. if not self.direct_columns:
  276. raise Exception(
  277. "Base Chart View property <direct_columns> must not be empty"
  278. )
  279. else:
  280. super(BaseSimpleDirectChartView, self).__init__(**kwargs)
  281. def get_group_by_columns(self):
  282. """
  283. returns the keys from direct_columns
  284. Used in template, so that user can choose from options
  285. """
  286. return list(self.direct_columns.keys())
  287. def _get_chart_widget(
  288. self,
  289. filters=None,
  290. order_column="",
  291. order_direction="",
  292. widgets=None,
  293. direct=None,
  294. height=None,
  295. **args
  296. ):
  297. height = height or self.height
  298. widgets = widgets or dict()
  299. joined_filters = filters.get_joined_filters(self._base_filters)
  300. count, lst = self.datamodel.query(
  301. filters=joined_filters,
  302. order_column=order_column,
  303. order_direction=order_direction,
  304. )
  305. value_columns = self.datamodel.get_values(lst, list(direct))
  306. value_columns = dict_to_json(
  307. direct[0], direct[1:], self.label_columns, value_columns
  308. )
  309. widgets["chart"] = self.chart_widget(
  310. route_base=self.route_base,
  311. chart_title=self.chart_title,
  312. chart_type=self.chart_type,
  313. chart_3d=self.chart_3d,
  314. height=height,
  315. value_columns=value_columns,
  316. modelview_name=self.__class__.__name__,
  317. **args
  318. )
  319. return widgets
  320. class ChartView(BaseSimpleGroupByChartView): # pragma: no cover
  321. """
  322. **DEPRECATED**
  323. Provides a simple (and hopefully nice) way to draw charts on your application.
  324. This will show Google Charts based on group by of your tables.
  325. """
  326. @expose("/chart/<group_by>")
  327. @expose("/chart/")
  328. @has_access
  329. def chart(self, group_by=""):
  330. form = self.search_form.refresh()
  331. get_filter_args(self._filters)
  332. group_by = group_by or self.group_by_columns[0]
  333. widgets = self._get_chart_widget(filters=self._filters, group_by=group_by)
  334. widgets = self._get_search_widget(form=form, widgets=widgets)
  335. return self.render_template(
  336. self.chart_template,
  337. route_base=self.route_base,
  338. title=self.chart_title,
  339. label_columns=self.label_columns,
  340. group_by_columns=self.group_by_columns,
  341. group_by_label=self.group_by_label,
  342. height=self.height,
  343. widgets=widgets,
  344. appbuilder=self.appbuilder,
  345. )
  346. class TimeChartView(BaseSimpleGroupByChartView): # pragma: no cover
  347. """
  348. **DEPRECATED**
  349. Provides a simple way to draw some time charts on your application.
  350. This will show Google Charts based on count and group
  351. by month and year for your tables.
  352. """
  353. chart_template = "appbuilder/general/charts/chart_time.html"
  354. chart_type = "ColumnChart"
  355. def _get_chart_widget(
  356. self,
  357. filters=None,
  358. order_column="",
  359. order_direction="",
  360. widgets=None,
  361. group_by=None,
  362. period=None,
  363. height=None,
  364. **args
  365. ):
  366. height = height or self.height
  367. widgets = widgets or dict()
  368. group_by = group_by or self.group_by_columns[0]
  369. joined_filters = filters.get_joined_filters(self._base_filters)
  370. if period == "month" or not period:
  371. value_columns = self.datamodel.query_month_group(
  372. group_by, filters=joined_filters
  373. )
  374. elif period == "year":
  375. value_columns = self.datamodel.query_year_group(
  376. group_by, filters=joined_filters
  377. )
  378. widgets["chart"] = self.chart_widget(
  379. route_base=self.route_base,
  380. chart_title=self.chart_title,
  381. chart_type=self.chart_type,
  382. chart_3d=self.chart_3d,
  383. height=height,
  384. value_columns=value_columns,
  385. modelview_name=self.__class__.__name__,
  386. **args
  387. )
  388. return widgets
  389. @expose("/chart/<group_by>/<period>")
  390. @expose("/chart/")
  391. @has_access
  392. def chart(self, group_by="", period=""):
  393. form = self.search_form.refresh()
  394. get_filter_args(self._filters)
  395. group_by = group_by or self.group_by_columns[0]
  396. widgets = self._get_chart_widget(
  397. filters=self._filters, group_by=group_by, period=period, height=self.height
  398. )
  399. widgets = self._get_search_widget(form=form, widgets=widgets)
  400. return self.render_template(
  401. self.chart_template,
  402. route_base=self.route_base,
  403. title=self.chart_title,
  404. label_columns=self.label_columns,
  405. group_by_columns=self.group_by_columns,
  406. group_by_label=self.group_by_label,
  407. widgets=widgets,
  408. appbuilder=self.appbuilder,
  409. )
  410. class DirectChartView(BaseSimpleDirectChartView): # pragma: no cover
  411. """
  412. **DEPRECATED**
  413. This class is responsible for displaying a Google chart with
  414. direct model values. Chart widget uses json.
  415. No group by is processed, example::
  416. class StatsChartView(DirectChartView):
  417. datamodel = SQLAInterface(Stats)
  418. chart_title = lazy_gettext('Statistics')
  419. direct_columns = {'Some Stats': ('X_col_1', 'stat_col_1', 'stat_col_2'),
  420. 'Other Stats': ('X_col2', 'stat_col_3')}
  421. """
  422. chart_type = "ColumnChart"
  423. chart_widget = DirectChartWidget
  424. @expose("/chart/<group_by>")
  425. @expose("/chart/")
  426. @has_access
  427. def chart(self, group_by=""):
  428. form = self.search_form.refresh()
  429. get_filter_args(self._filters)
  430. direct_key = group_by or list(self.direct_columns.keys())[0]
  431. direct = self.direct_columns.get(direct_key)
  432. if self.base_order:
  433. order_column, order_direction = self.base_order
  434. else:
  435. order_column, order_direction = "", ""
  436. widgets = self._get_chart_widget(
  437. filters=self._filters,
  438. order_column=order_column,
  439. order_direction=order_direction,
  440. direct=direct,
  441. )
  442. widgets = self._get_search_widget(form=form, widgets=widgets)
  443. return self.render_template(
  444. self.chart_template,
  445. route_base=self.route_base,
  446. title=self.chart_title,
  447. label_columns=self.label_columns,
  448. group_by_columns=self.get_group_by_columns(),
  449. group_by_label=self.group_by_label,
  450. height=self.height,
  451. widgets=widgets,
  452. appbuilder=self.appbuilder,
  453. )