From 75ebec6ec8876064c993590b87ffc701e0d5f60a Mon Sep 17 00:00:00 2001 From: Ramez Ashraf Date: Thu, 27 Apr 2023 19:41:11 +0200 Subject: [PATCH 01/20] Better weekly time series default name Add time_series_selector --- slick_reporting/fields.py | 2 +- slick_reporting/form_factory.py | 22 +++++++++++++++++++++- slick_reporting/views.py | 23 ++++++++++++++++++----- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/slick_reporting/fields.py b/slick_reporting/fields.py index bdd0e48..a8cf8cc 100644 --- a/slick_reporting/fields.py +++ b/slick_reporting/fields.py @@ -327,7 +327,7 @@ def get_time_series_field_verbose_name(cls, date_period, index, dates, pattern): elif pattern == 'daily': return f'{cls.verbose_name} {date_period[0].strftime(dt_format)}' elif pattern == 'weekly': - return f' {cls.verbose_name} {_("Week")} {index} {date_period[0].strftime(dt_format)}' + return f' {cls.verbose_name} {_("Week")} {index + 1} {date_period[0].strftime(dt_format)}' elif pattern == 'yearly': year = date_filter(date_period[0], 'Y') return f'{cls.verbose_name} {year}' diff --git a/slick_reporting/form_factory.py b/slick_reporting/form_factory.py index affcdb1..95e2f61 100644 --- a/slick_reporting/form_factory.py +++ b/slick_reporting/form_factory.py @@ -7,6 +7,13 @@ from . import app_settings from .helpers import get_foreign_keys +TIME_SERIES_CHOICES = ( + ('monthly', _('Monthly')), + ('weekly', _('Weekly')), + ('annually', _('Yearly')), + ('daily', _('Daily')), +) + class BaseReportForm: ''' @@ -100,7 +107,11 @@ def _default_foreign_key_widget(f_field): def report_form_factory(model, crosstab_model=None, display_compute_reminder=True, fkeys_filter_func=None, - foreign_key_widget_func=None, excluded_fields=None, initial=None, required=None): + foreign_key_widget_func=None, excluded_fields=None, initial=None, required=None, + show_time_series_selector=False, + time_series_selector_choices=None, time_series_selector_default='', + time_series_selector_label=None, + time_series_selector_allow_empty=False): """ Create a Report Form based on the report_model passed by 1. adding a start_date and end_date fields @@ -143,6 +154,15 @@ def report_form_factory(model, crosstab_model=None, display_compute_reminder=Tru app_settings.SLICK_REPORTING_DEFAULT_END_DATE), widget=forms.DateTimeInput(attrs={'autocomplete': "off"})) + if show_time_series_selector: + time_series_choices = tuple(TIME_SERIES_CHOICES) + if time_series_selector_allow_empty: + time_series_choices.insert(0, ('', '---------')) + + fields['time_series_pattern'] = forms.ChoiceField(required=False, initial=time_series_selector_default, + label=time_series_selector_label or _('Period Pattern'), + choices=time_series_selector_choices or TIME_SERIES_CHOICES) + for name, f_field in fkeys_map.items(): fkeys_list.append(name) field_attrs = foreign_key_widget_func(f_field) diff --git a/slick_reporting/views.py b/slick_reporting/views.py index 2bf40d7..20df25e 100644 --- a/slick_reporting/views.py +++ b/slick_reporting/views.py @@ -42,6 +42,12 @@ class SlickReportViewBase(FormView): crosstab_compute_reminder = True excluded_fields = None report_title_context_key = 'title' + + time_series_selector = False + time_series_selector_choices = None + time_series_selector_default = None + time_series_selector_allow_empty = False + """ A list of chart settings objects instructing front end on how to plot the data. @@ -95,7 +101,10 @@ def get_form_class(self): display_compute_reminder=self.crosstab_compute_reminder, excluded_fields=self.excluded_fields, initial=self.get_form_initial(), - # required=self.required_fields + show_time_series_selector=cls.time_series_selector, + time_series_selector_choices=cls.time_series_selector_choices, + time_series_selector_default=cls.time_series_selector_default + ) def get_form_kwargs(self): @@ -129,6 +138,10 @@ def get_report_generator(self, queryset, for_print): crosstab_compute_reminder = self.form.get_crosstab_compute_reminder() if self.request.GET or self.request.POST \ else self.crosstab_compute_reminder + time_series_pattern = self.time_series_pattern + if self.time_series_selector: + time_series_pattern = self.form.cleaned_data['time_series_pattern'] + return self.report_generator_class(self.get_report_model(), start_date=self.form.cleaned_data['start_date'], end_date=self.form.cleaned_data['end_date'], @@ -140,7 +153,7 @@ def get_report_generator(self, queryset, for_print): limit_records=self.limit_records, swap_sign=self.swap_sign, columns=self.columns, group_by=self.group_by, - time_series_pattern=self.time_series_pattern, + time_series_pattern=time_series_pattern, time_series_columns=self.time_series_columns, crosstab_model=self.crosstab_model, @@ -180,8 +193,8 @@ def get_report_results(self, for_print=False): data = self.filter_results(data, for_print) return report_generator.get_full_response(data=data, report_slug=self.get_report_slug(), - chart_settings=self.chart_settings, - default_chart_title=self.report_title) + chart_settings=self.chart_settings, + default_chart_title=self.report_title) @classmethod def get_metadata(cls, generator): @@ -245,5 +258,5 @@ def __init_subclass__(cls) -> None: @staticmethod def check_chart_settings(chart_settings=None): - #todo check on chart settings + # todo check on chart settings return From ec9caa725faf467242e16acaa35a6617b60e2994 Mon Sep 17 00:00:00 2001 From: Ramez Ashraf Date: Thu, 27 Apr 2023 19:41:11 +0200 Subject: [PATCH 02/20] Better weekly time series default name Add time_series_selector --- slick_reporting/fields.py | 2 +- slick_reporting/form_factory.py | 22 +++++++++++++++++++++- slick_reporting/views.py | 23 ++++++++++++++++++----- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/slick_reporting/fields.py b/slick_reporting/fields.py index bdd0e48..a8cf8cc 100644 --- a/slick_reporting/fields.py +++ b/slick_reporting/fields.py @@ -327,7 +327,7 @@ def get_time_series_field_verbose_name(cls, date_period, index, dates, pattern): elif pattern == 'daily': return f'{cls.verbose_name} {date_period[0].strftime(dt_format)}' elif pattern == 'weekly': - return f' {cls.verbose_name} {_("Week")} {index} {date_period[0].strftime(dt_format)}' + return f' {cls.verbose_name} {_("Week")} {index + 1} {date_period[0].strftime(dt_format)}' elif pattern == 'yearly': year = date_filter(date_period[0], 'Y') return f'{cls.verbose_name} {year}' diff --git a/slick_reporting/form_factory.py b/slick_reporting/form_factory.py index affcdb1..95e2f61 100644 --- a/slick_reporting/form_factory.py +++ b/slick_reporting/form_factory.py @@ -7,6 +7,13 @@ from . import app_settings from .helpers import get_foreign_keys +TIME_SERIES_CHOICES = ( + ('monthly', _('Monthly')), + ('weekly', _('Weekly')), + ('annually', _('Yearly')), + ('daily', _('Daily')), +) + class BaseReportForm: ''' @@ -100,7 +107,11 @@ def _default_foreign_key_widget(f_field): def report_form_factory(model, crosstab_model=None, display_compute_reminder=True, fkeys_filter_func=None, - foreign_key_widget_func=None, excluded_fields=None, initial=None, required=None): + foreign_key_widget_func=None, excluded_fields=None, initial=None, required=None, + show_time_series_selector=False, + time_series_selector_choices=None, time_series_selector_default='', + time_series_selector_label=None, + time_series_selector_allow_empty=False): """ Create a Report Form based on the report_model passed by 1. adding a start_date and end_date fields @@ -143,6 +154,15 @@ def report_form_factory(model, crosstab_model=None, display_compute_reminder=Tru app_settings.SLICK_REPORTING_DEFAULT_END_DATE), widget=forms.DateTimeInput(attrs={'autocomplete': "off"})) + if show_time_series_selector: + time_series_choices = tuple(TIME_SERIES_CHOICES) + if time_series_selector_allow_empty: + time_series_choices.insert(0, ('', '---------')) + + fields['time_series_pattern'] = forms.ChoiceField(required=False, initial=time_series_selector_default, + label=time_series_selector_label or _('Period Pattern'), + choices=time_series_selector_choices or TIME_SERIES_CHOICES) + for name, f_field in fkeys_map.items(): fkeys_list.append(name) field_attrs = foreign_key_widget_func(f_field) diff --git a/slick_reporting/views.py b/slick_reporting/views.py index 2bf40d7..d1e3d64 100644 --- a/slick_reporting/views.py +++ b/slick_reporting/views.py @@ -42,6 +42,12 @@ class SlickReportViewBase(FormView): crosstab_compute_reminder = True excluded_fields = None report_title_context_key = 'title' + + time_series_selector = False + time_series_selector_choices = None + time_series_selector_default = None + time_series_selector_allow_empty = False + """ A list of chart settings objects instructing front end on how to plot the data. @@ -95,7 +101,10 @@ def get_form_class(self): display_compute_reminder=self.crosstab_compute_reminder, excluded_fields=self.excluded_fields, initial=self.get_form_initial(), - # required=self.required_fields + show_time_series_selector=cls.time_series_selector, + time_series_selector_choices=cls.time_series_selector_choices, + time_series_selector_default=cls.time_series_selector_default, + time_series_selector_allow_empty=cls.time_series_selector_allow_empty, ) def get_form_kwargs(self): @@ -129,6 +138,10 @@ def get_report_generator(self, queryset, for_print): crosstab_compute_reminder = self.form.get_crosstab_compute_reminder() if self.request.GET or self.request.POST \ else self.crosstab_compute_reminder + time_series_pattern = self.time_series_pattern + if self.time_series_selector: + time_series_pattern = self.form.cleaned_data['time_series_pattern'] + return self.report_generator_class(self.get_report_model(), start_date=self.form.cleaned_data['start_date'], end_date=self.form.cleaned_data['end_date'], @@ -140,7 +153,7 @@ def get_report_generator(self, queryset, for_print): limit_records=self.limit_records, swap_sign=self.swap_sign, columns=self.columns, group_by=self.group_by, - time_series_pattern=self.time_series_pattern, + time_series_pattern=time_series_pattern, time_series_columns=self.time_series_columns, crosstab_model=self.crosstab_model, @@ -180,8 +193,8 @@ def get_report_results(self, for_print=False): data = self.filter_results(data, for_print) return report_generator.get_full_response(data=data, report_slug=self.get_report_slug(), - chart_settings=self.chart_settings, - default_chart_title=self.report_title) + chart_settings=self.chart_settings, + default_chart_title=self.report_title) @classmethod def get_metadata(cls, generator): @@ -245,5 +258,5 @@ def __init_subclass__(cls) -> None: @staticmethod def check_chart_settings(chart_settings=None): - #todo check on chart settings + # todo check on chart settings return From 1117732c6c300055fe898c165c20bf59dcdd7278 Mon Sep 17 00:00:00 2001 From: Ramez Ashraf Date: Sat, 29 Apr 2023 17:34:14 +0300 Subject: [PATCH 03/20] Add container class method resolution --- slick_reporting/generator.py | 31 ++++++++++++++++++++++--------- slick_reporting/views.py | 13 +++++++------ tests/test_settings.py | 2 ++ 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/slick_reporting/generator.py b/slick_reporting/generator.py index 00f812c..1eabb35 100644 --- a/slick_reporting/generator.py +++ b/slick_reporting/generator.py @@ -124,7 +124,8 @@ def __init__(self, report_model=None, main_queryset=None, start_date=None, end_d crosstab_model=None, crosstab_columns=None, crosstab_ids=None, crosstab_compute_reminder=None, swap_sign=False, show_empty_records=None, print_flag=False, - doc_type_plus_list=None, doc_type_minus_list=None, limit_records=False, format_row_func=None): + doc_type_plus_list=None, doc_type_minus_list=None, limit_records=False, format_row_func=None, + container_class=None): """ :param report_model: Main model containing the data @@ -186,6 +187,7 @@ def __init__(self, report_model=None, main_queryset=None, start_date=None, end_d self.time_series_pattern = self.time_series_pattern or time_series_pattern self.time_series_columns = self.time_series_columns or time_series_columns self.time_series_custom_dates = self.time_series_custom_dates or time_series_custom_dates + self.container_class = container_class self._prepared_results = {} self.report_fields_classes = {} @@ -309,7 +311,8 @@ def _prepare_report_dependencies(self): # check if any of these dependencies is on the report, if found we call the child to # resolve the value for its parent avoiding extra database call fields_on_report = [x for x in window_cols if x['ref'] in dependencies_names - and ((window == 'time_series' and x.get('start_date', '') == col_data.get('start_date', '') and x.get('end_date') == col_data.get('end_date')) or + and ((window == 'time_series' and x.get('start_date', '') == col_data.get( + 'start_date', '') and x.get('end_date') == col_data.get('end_date')) or window == 'crosstab' and x.get('id') == col_data.get('id'))] for field in fields_on_report: self._report_fields_dependencies[window][field['name']] = col_data['name'] @@ -372,6 +375,8 @@ def _get_record_data(self, obj, columns): if col_data.get('source', '') == 'attribute_field': data[name] = col_data['ref'](self, obj, data) + elif col_data.get('source', '') == 'container_class_attribute_field': + data[name] = col_data['ref'](obj, data) elif (col_data.get('source', '') == 'magic_field' and self.group_by) or ( self.time_series_pattern and not self.group_by): @@ -416,7 +421,7 @@ def _default_format_row(self, row_obj): return row_obj @classmethod - def check_columns(cls, columns, group_by, report_model, ): + def check_columns(cls, columns, group_by, report_model, container_class=None): """ Check and parse the columns, throw errors in case an item in the columns cant not identified :param columns: List of columns @@ -448,9 +453,13 @@ def check_columns(cls, columns, group_by, report_model, ): magic_field_class = None attribute_field = None - + is_container_class_attribute = False if type(col) is str: attribute_field = getattr(cls, col, None) + if attribute_field is None: + is_container_class_attribute = True + attribute_field = getattr(container_class, col, None) + elif issubclass(col, SlickReportField): magic_field_class = col @@ -462,7 +471,7 @@ def check_columns(cls, columns, group_by, report_model, ): if attribute_field: col_data = {'name': col, 'verbose_name': getattr(attribute_field, 'verbose_name', col), - 'source': 'attribute_field', + 'source': 'container_class_attribute_field' if is_container_class_attribute else 'attribute_field', 'ref': attribute_field, 'type': 'text' } @@ -490,9 +499,13 @@ def check_columns(cls, columns, group_by, report_model, ): else: field = model_to_use._meta.get_field(col) except FieldDoesNotExist: - raise FieldDoesNotExist( - f'Field "{col}" not found either as an attribute to the generator class {cls}, ' - f'or a computation field, or a database column for the model "{model_to_use}"') + field = getattr(container_class, col, False) + + if not field: + raise FieldDoesNotExist( + f'Field "{col}" not found either as an attribute to the generator class {cls}, ' + f'{f"Container class {container_class}," if container_class else ""}' + f'or a computation field, or a database column for the model "{model_to_use}"') col_data = {'name': col, 'verbose_name': getattr(field, 'verbose_name', col), @@ -505,7 +518,7 @@ def check_columns(cls, columns, group_by, report_model, ): return parsed_columns def _parse(self): - self.parsed_columns = self.check_columns(self.columns, self.group_by, self.report_model) + self.parsed_columns = self.check_columns(self.columns, self.group_by, self.report_model, self.container_class) self._parsed_columns = list(self.parsed_columns) self._time_series_parsed_columns = self.get_time_series_parsed_columns() self._crosstab_parsed_columns = self.get_crosstab_parsed_columns() diff --git a/slick_reporting/views.py b/slick_reporting/views.py index d1e3d64..961f8b9 100644 --- a/slick_reporting/views.py +++ b/slick_reporting/views.py @@ -101,10 +101,10 @@ def get_form_class(self): display_compute_reminder=self.crosstab_compute_reminder, excluded_fields=self.excluded_fields, initial=self.get_form_initial(), - show_time_series_selector=cls.time_series_selector, - time_series_selector_choices=cls.time_series_selector_choices, - time_series_selector_default=cls.time_series_selector_default, - time_series_selector_allow_empty=cls.time_series_selector_allow_empty, + show_time_series_selector=self.time_series_selector, + time_series_selector_choices=self.time_series_selector_choices, + time_series_selector_default=self.time_series_selector_default, + time_series_selector_allow_empty=self.time_series_selector_allow_empty, ) def get_form_kwargs(self): @@ -161,7 +161,8 @@ def get_report_generator(self, queryset, for_print): crosstab_columns=self.crosstab_columns, crosstab_compute_reminder=crosstab_compute_reminder, - format_row_func=self.format_row + format_row_func=self.format_row, + container_class=self ) def format_row(self, row_obj): @@ -252,7 +253,7 @@ def __init_subclass__(cls) -> None: # sanity check, raises error if the columns or date fields is not mapped cls.report_generator_class.check_columns([cls.date_field], False, cls.get_report_model()) - cls.report_generator_class.check_columns(cls.columns, cls.group_by, cls.get_report_model()) + cls.report_generator_class.check_columns(cls.columns, cls.group_by, cls.get_report_model(), container_class=cls) super().__init_subclass__() diff --git a/tests/test_settings.py b/tests/test_settings.py index b3b7b46..06644f6 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -59,3 +59,5 @@ MIGRATION_MODULES = {'contenttypes': None, 'auth': None} DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +CRISPY_TEMPLATE_PACK = 'bootstrap4' From 74f1141b7cd18cfa66e27ddeb9ea605ad8ef1d13 Mon Sep 17 00:00:00 2001 From: Ramez Ashraf Date: Sat, 29 Apr 2023 21:21:04 +0300 Subject: [PATCH 04/20] Make date field optional --- slick_reporting/generator.py | 18 +++++++++++------- slick_reporting/views.py | 11 +++++++---- tests/test_generator.py | 2 ++ 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/slick_reporting/generator.py b/slick_reporting/generator.py index 1eabb35..d24a977 100644 --- a/slick_reporting/generator.py +++ b/slick_reporting/generator.py @@ -165,8 +165,6 @@ def __init__(self, report_model=None, main_queryset=None, start_date=None, end_d self.end_date = end_date or datetime.datetime.combine(SLICK_REPORTING_DEFAULT_END_DATE.date(), SLICK_REPORTING_DEFAULT_END_DATE.time()) self.date_field = self.date_field or date_field - if not self.date_field: - raise ImproperlyConfigured('date_field must be set on a class level or via init') self.q_filters = q_filters or [] self.kwargs_filters = kwargs_filters or {} @@ -189,6 +187,10 @@ def __init__(self, report_model=None, main_queryset=None, start_date=None, end_d self.time_series_custom_dates = self.time_series_custom_dates or time_series_custom_dates self.container_class = container_class + if not self.date_field and (self.time_series_pattern or self.crosstab_model or self.group_by): + raise ImproperlyConfigured('date_field must be set on a class level or via init') + + self._prepared_results = {} self.report_fields_classes = {} @@ -269,11 +271,12 @@ def _apply_queryset_options(self, query, fields=None): :param fields: :return: """ - - filters = { - f'{self.date_field}__gt': self.start_date, - f'{self.date_field}__lte': self.end_date, - } + filters = {} + if self.date_field: + filters = { + f'{self.date_field}__gt': self.start_date, + f'{self.date_field}__lte': self.end_date, + } filters.update(self.kwargs_filters) if filters: @@ -454,6 +457,7 @@ def check_columns(cls, columns, group_by, report_model, container_class=None): magic_field_class = None attribute_field = None is_container_class_attribute = False + if type(col) is str: attribute_field = getattr(cls, col, None) if attribute_field is None: diff --git a/slick_reporting/views.py b/slick_reporting/views.py index 961f8b9..bb78819 100644 --- a/slick_reporting/views.py +++ b/slick_reporting/views.py @@ -48,6 +48,8 @@ class SlickReportViewBase(FormView): time_series_selector_default = None time_series_selector_allow_empty = False + use_queryset_values = True + """ A list of chart settings objects instructing front end on how to plot the data. @@ -247,12 +249,13 @@ def get_context_data(self, **kwargs): class SlickReportView(SlickReportViewBase): def __init_subclass__(cls) -> None: - date_field = getattr(cls, 'date_field', '') - if not date_field: - raise TypeError(f'`date_field` is not set on {cls}') + # date_field = getattr(cls, 'date_field', '') + # if not date_field: + # raise TypeError(f'`date_field` is not set on {cls}') + # cls.report_generator_class.check_columns([cls.date_field], False, cls.get_report_model()) # sanity check, raises error if the columns or date fields is not mapped - cls.report_generator_class.check_columns([cls.date_field], False, cls.get_report_model()) + cls.report_generator_class.check_columns(cls.columns, cls.group_by, cls.get_report_model(), container_class=cls) super().__init_subclass__() diff --git a/tests/test_generator.py b/tests/test_generator.py index 438544c..6df8a5b 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -1,4 +1,5 @@ from datetime import datetime +from unittest import skip import pytz from django.db.models import Sum @@ -153,6 +154,7 @@ def load(): self.assertRaises(Exception, load) + @skip('Maybe we should not raise error on missing dates ') def test_missing_date_field(self): def load(): ReportGenerator(report_model=OrderLine, group_by='product', date_field='') From 3d01ca3faf9845a76570ad716d2420fe260efe7f Mon Sep 17 00:00:00 2001 From: Ramez Ashraf Date: Sat, 29 Apr 2023 23:43:20 +0300 Subject: [PATCH 05/20] SlickReportingListView init --- slick_reporting/app_settings.py | 6 ++- slick_reporting/generator.py | 80 ++++++++++++++++++++++++++++++++- slick_reporting/views.py | 37 +++++++++++++-- 3 files changed, 116 insertions(+), 7 deletions(-) diff --git a/slick_reporting/app_settings.py b/slick_reporting/app_settings.py index 2919e23..a0c6071 100644 --- a/slick_reporting/app_settings.py +++ b/slick_reporting/app_settings.py @@ -4,6 +4,8 @@ import datetime +from django.utils.timezone import now + def get_first_of_this_year(): d = datetime.datetime.today() @@ -21,8 +23,8 @@ def get_start_date(): def get_end_date(): - start_date = getattr(settings, 'SLICK_REPORTING_DEFAULT_END_DATE', False) - return start_date or get_end_of_this_year() + end_date = getattr(settings, 'SLICK_REPORTING_DEFAULT_END_DATE', False) + return end_date or now() #get_end_of_this_year() SLICK_REPORTING_DEFAULT_START_DATE = lazy(get_start_date, datetime.datetime)() diff --git a/slick_reporting/generator.py b/slick_reporting/generator.py index d24a977..b5780ac 100644 --- a/slick_reporting/generator.py +++ b/slick_reporting/generator.py @@ -190,7 +190,6 @@ def __init__(self, report_model=None, main_queryset=None, start_date=None, end_d if not self.date_field and (self.time_series_pattern or self.crosstab_model or self.group_by): raise ImproperlyConfigured('date_field must be set on a class level or via init') - self._prepared_results = {} self.report_fields_classes = {} @@ -515,7 +514,7 @@ def check_columns(cls, columns, group_by, report_model, container_class=None): 'verbose_name': getattr(field, 'verbose_name', col), 'source': 'database', 'ref': field, - 'type': field.get_internal_type() + 'type': 'choice' if field.choices else field.get_internal_type(), } col_data.update(options) parsed_columns.append(col_data) @@ -749,3 +748,80 @@ def get_chart_settings(self, chart_settings=None, default_chart_title=None): x['engine_name'] = x.get('engine_name', SLICK_REPORTING_DEFAULT_CHARTS_ENGINE) output.append(x) return output + + +class ListViewReportGenerator(ReportGenerator): + + def _apply_queryset_options(self, query, fields=None): + """ + Apply the filters to the main queryset which will computed results be mapped to + :param query: + :param fields: + :return: + """ + filters = {} + if self.date_field: + filters = { + f'{self.date_field}__gt': self.start_date, + f'{self.date_field}__lte': self.end_date, + } + filters.update(self.kwargs_filters) + + if filters: + query = query.filter(**filters) + # if fields: + # return query.values(*fields) + return query + + def _get_record_data(self, obj, columns): + """ + the function is run for every obj in the main_queryset + :param obj: current row + :param: columns: The columns we iterate on + :return: a dict object containing all needed data + """ + + data = {} + group_by_val = None + if self.group_by: + if self.group_by_field.related_model and '__' not in self.group_by: + primary_key_name = self.get_primary_key_name(self.group_by_field.related_model) + else: + primary_key_name = self.group_by_field_attname + + column_data = obj.get(primary_key_name, obj.get('id')) + group_by_val = str(column_data) + + for window, window_cols in columns: + for col_data in window_cols: + + name = col_data['name'] + + if col_data.get('source', '') == 'attribute_field': + data[name] = col_data['ref'](self, obj, data) + # changed line + elif col_data.get('source', '') == 'container_class_attribute_field': + data[name] = col_data['ref'](obj) + + elif (col_data.get('source', '') == 'magic_field' and self.group_by) or ( + self.time_series_pattern and not self.group_by): + source = self._report_fields_dependencies[window].get(name, False) + if source: + computation_class = self.report_fields_classes[source] + value = computation_class.get_dependency_value(group_by_val, + col_data['ref'].name) + else: + try: + computation_class = self.report_fields_classes[name] + except KeyError: + continue + value = computation_class.resolve(group_by_val, data) + if self.swap_sign: value = -value + data[name] = value + + else: + if col_data.get('type', '') == 'choice': + data[name] = getattr(obj, f"get_{name}_display", '')() + else: + data[name] = getattr(obj, name, '') + return data diff --git a/slick_reporting/views.py b/slick_reporting/views.py index bb78819..de1138a 100644 --- a/slick_reporting/views.py +++ b/slick_reporting/views.py @@ -10,7 +10,7 @@ from .app_settings import SLICK_REPORTING_DEFAULT_END_DATE, SLICK_REPORTING_DEFAULT_START_DATE, \ SLICK_REPORTING_DEFAULT_CHARTS_ENGINE from .form_factory import report_form_factory -from .generator import ReportGenerator +from .generator import ReportGenerator, ListViewReportGenerator class SlickReportViewBase(FormView): @@ -48,8 +48,6 @@ class SlickReportViewBase(FormView): time_series_selector_default = None time_series_selector_allow_empty = False - use_queryset_values = True - """ A list of chart settings objects instructing front end on how to plot the data. @@ -264,3 +262,36 @@ def __init_subclass__(cls) -> None: def check_chart_settings(chart_settings=None): # todo check on chart settings return + + +class SlickReportingListView(SlickReportViewBase): + # todo create form easily + + report_generator_class = ListViewReportGenerator + + def get(self, request, *args, **kwargs): + form_class = self.get_form_class() + self.form = self.get_form(form_class) + if self.form.is_valid(): + report_data = self.get_report_results() + if request.headers.get('x-requested-with') == 'XMLHttpRequest': + return self.ajax_render_to_response(report_data) + + return self.render_to_response(self.get_context_data(report_data=report_data)) + + return self.render_to_response(self.get_context_data()) + + def get_report_results(self, for_print=False): + """ + Gets the reports Data, and, its meta data used by datatables.net and highcharts + :return: JsonResponse + """ + + queryset = self.get_queryset() + report_generator = self.get_report_generator(queryset, for_print) + data = report_generator.get_report_data() + data = self.filter_results(data, for_print) + + return report_generator.get_full_response(data=data, report_slug=self.get_report_slug(), + chart_settings=self.chart_settings, + default_chart_title=self.report_title) From 887d07a2761bbfbf73017607976d56bdb8da0d0c Mon Sep 17 00:00:00 2001 From: Ramez Ashraf Date: Sun, 30 Apr 2023 01:05:03 +0300 Subject: [PATCH 06/20] Enhance crispy helper Enhance SlickReportingListView easier filters --- slick_reporting/form_factory.py | 86 ++++++++++++++++++++------------- slick_reporting/views.py | 68 ++++++++++++++++++++++++-- 2 files changed, 117 insertions(+), 37 deletions(-) diff --git a/slick_reporting/form_factory.py b/slick_reporting/form_factory.py index 95e2f61..6cc3f9e 100644 --- a/slick_reporting/form_factory.py +++ b/slick_reporting/form_factory.py @@ -15,6 +15,52 @@ ) +def default_formfield_callback(f, **kwargs): + kwargs['required'] = False + kwargs['help_text'] = '' + return f.formfield(**kwargs) + + +def get_crispy_helper(foreign_keys_map=None, crosstab_model=None, crosstab_key_name=None, + crosstab_display_compute_reminder=False, add_date_range=True): + from crispy_forms.helper import FormHelper + from crispy_forms.layout import Column, Layout, Div, Row, Field + + helper = FormHelper() + helper.form_class = 'form-horizontal' + helper.label_class = 'col-sm-2 col-md-2 col-lg-2' + helper.field_class = 'col-sm-10 col-md-10 col-lg-10' + helper.form_tag = False + helper.disable_csrf = True + helper.render_unmentioned_fields = True + + helper.layout = Layout( + + ) + if add_date_range: + helper.layout.fields.append( + Row( + Column( + Field('start_date'), css_class='col-sm-6'), + Column( + Field('end_date'), css_class='col-sm-6'), + css_class='raReportDateRange'), + ) + filters_container = Div(css_class="mt-20", style='margin-top:20px') + # first add the crosstab model and its display reimder then the rest of the fields + if crosstab_model: + filters_container.append(Field(crosstab_key_name)) + if crosstab_display_compute_reminder: + filters_container.append(Field('crosstab_compute_reminder')) + + for k in foreign_keys_map: + if k != crosstab_key_name: + filters_container.append(Field(k)) + helper.layout.fields.append(filters_container) + + return helper + + class BaseReportForm: ''' Holds basic function @@ -66,40 +112,12 @@ def get_crosstab_compute_reminder(self): return self.cleaned_data.get('crosstab_compute_reminder', True) def get_crispy_helper(self, foreign_keys_map=None, crosstab_model=None, **kwargs): - from crispy_forms.helper import FormHelper - from crispy_forms.layout import Column, Layout, Div, Row, Field - - helper = FormHelper() - helper.form_class = 'form-horizontal' - helper.label_class = 'col-sm-2 col-md-2 col-lg-2' - helper.field_class = 'col-sm-10 col-md-10 col-lg-10' - helper.form_tag = False - helper.disable_csrf = True - helper.render_unmentioned_fields = True - - foreign_keys_map = foreign_keys_map or self.foreign_keys - - helper.layout = Layout( - Row( - Column( - Field('start_date'), css_class='col-sm-6'), - Column( - Field('end_date'), css_class='col-sm-6'), - css_class='raReportDateRange'), - Div(css_class="mt-20", style='margin-top:20px') - ) - - # first add the crosstab model and its display reimder then the rest of the fields - if self.crosstab_model: - helper.layout.fields[1].append(Field(self.crosstab_key_name)) - if self.crosstab_display_compute_reminder: - helper.layout.fields[1].append(Field('crosstab_compute_reminder')) - - for k in foreign_keys_map: - if k != self.crosstab_key_name: - helper.layout.fields[1].append(Field(k)) - - return helper + return get_crispy_helper(self.foreign_keys, + crosstab_model=getattr(self, 'crosstab_model', None), + crosstab_key_name=getattr(self, 'crosstab_key_name', None), + crosstab_display_compute_reminder=getattr(self, 'crosstab_display_compute_reminder', + False), + **kwargs) def _default_foreign_key_widget(f_field): diff --git a/slick_reporting/views.py b/slick_reporting/views.py index de1138a..ab2d3ce 100644 --- a/slick_reporting/views.py +++ b/slick_reporting/views.py @@ -1,7 +1,9 @@ import datetime import simplejson as json +from django import forms from django.conf import settings +from django.forms import modelform_factory from django.http import HttpResponse from django.utils.encoding import force_str from django.utils.functional import Promise @@ -9,7 +11,7 @@ from .app_settings import SLICK_REPORTING_DEFAULT_END_DATE, SLICK_REPORTING_DEFAULT_START_DATE, \ SLICK_REPORTING_DEFAULT_CHARTS_ENGINE -from .form_factory import report_form_factory +from .form_factory import report_form_factory, get_crispy_helper, default_formfield_callback from .generator import ReportGenerator, ListViewReportGenerator @@ -235,9 +237,14 @@ def get_form_initial(): 'end_date': SLICK_REPORTING_DEFAULT_END_DATE } + def get_form_crispy_helper(self): + return self.form.get_crispy_helper() + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context[self.report_title_context_key] = self.report_title + context['crispy_helper'] = self.get_form_crispy_helper() + if not (self.request.POST or self.request.GET): # initialize empty form with initials if the no data is in the get or the post context['form'] = self.get_form_class()() @@ -265,9 +272,64 @@ def check_chart_settings(chart_settings=None): class SlickReportingListView(SlickReportViewBase): - # todo create form easily - report_generator_class = ListViewReportGenerator + filters = None + + def get_form_filters(self, form): + kw_filters = {} + + for name, field in form.base_fields.items(): + + if type(field) is forms.ModelMultipleChoiceField: + value = form.cleaned_data[name] + if value: + kw_filters[f'{name}__in'] = form.cleaned_data[name] + elif type(field) is forms.BooleanField: + kw_filters[name] = form.cleaned_data[name] + else: + value = form.cleaned_data[name] + if value: + kw_filters[name] = form.cleaned_data[name] + + return [], kw_filters + + def get_form_crispy_helper(self): + return get_crispy_helper(self.filters) + + def get_report_generator(self, queryset, for_print): + q_filters, kw_filters = self.get_form_filters(self.form) + + crosstab_compute_reminder = False + + time_series_pattern = self.time_series_pattern + + return self.report_generator_class(self.get_report_model(), + # start_date=self.form.cleaned_data['start_date'], + # end_date=self.form.cleaned_data['end_date'], + q_filters=q_filters, + kwargs_filters=kw_filters, + date_field=self.date_field, + main_queryset=queryset, + print_flag=for_print, + limit_records=self.limit_records, swap_sign=self.swap_sign, + columns=self.columns, + group_by=self.group_by, + time_series_pattern=time_series_pattern, + time_series_columns=self.time_series_columns, + + crosstab_model=self.crosstab_model, + crosstab_ids=self.crosstab_ids, + crosstab_columns=self.crosstab_columns, + crosstab_compute_reminder=crosstab_compute_reminder, + + format_row_func=self.format_row, + container_class=self + ) + + def get_form_class(self): + + return modelform_factory(self.get_report_model(), fields=self.filters, + formfield_callback=default_formfield_callback) def get(self, request, *args, **kwargs): form_class = self.get_form_class() From e8e5df5384c62d7431b2548bf3f31b686962adb1 Mon Sep 17 00:00:00 2001 From: Ramez Ashraf Date: Sun, 30 Apr 2023 01:48:24 +0300 Subject: [PATCH 07/20] Handle case boolean field in ListReport form --- slick_reporting/views.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/slick_reporting/views.py b/slick_reporting/views.py index ab2d3ce..a87bdbf 100644 --- a/slick_reporting/views.py +++ b/slick_reporting/views.py @@ -128,7 +128,6 @@ def get_form_kwargs(self): # elif self.request.GET: kwargs.update({ 'data': self.request.GET, - 'files': self.request.FILES, }) return kwargs @@ -285,7 +284,9 @@ def get_form_filters(self, form): if value: kw_filters[f'{name}__in'] = form.cleaned_data[name] elif type(field) is forms.BooleanField: - kw_filters[name] = form.cleaned_data[name] + # boolean field while checked on frontend , and have initial = True, give false value on cleaned_data + # Hence this check to see if it was indeed in the GET params, + kw_filters[name] = form.cleaned_data[name] if name in self.request.GET else field.initial else: value = form.cleaned_data[name] if value: @@ -311,16 +312,9 @@ def get_report_generator(self, queryset, for_print): date_field=self.date_field, main_queryset=queryset, print_flag=for_print, - limit_records=self.limit_records, swap_sign=self.swap_sign, - columns=self.columns, - group_by=self.group_by, - time_series_pattern=time_series_pattern, - time_series_columns=self.time_series_columns, + limit_records=self.limit_records, - crosstab_model=self.crosstab_model, - crosstab_ids=self.crosstab_ids, - crosstab_columns=self.crosstab_columns, - crosstab_compute_reminder=crosstab_compute_reminder, + columns=self.columns, format_row_func=self.format_row, container_class=self From fa27051e3252efa18f4de1a9be8d0d0f69cf9a77 Mon Sep 17 00:00:00 2001 From: Ramez Ashraf Date: Sun, 30 Apr 2023 23:17:02 +0300 Subject: [PATCH 08/20] CHANGELOG.md --- CHANGELOG.md | 41 +++++++++++++++++++++++----------------- slick_reporting/views.py | 4 ---- tests/test_generator.py | 1 - 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66dba28..b09670e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,38 +2,48 @@ All notable changes to this project will be documented in this file. +## [0.7.0] + +- Added SlickReportingListView +- Added `show_time_series_selector` capability to SlickReportView allowing User to change the time series pattern from + the UI. +- Now you can have a custom column defined on the SlickReportView and not needing to customise the report generator. +- You don't need to set date_field if you have calculations on the report +- Easier customization of the crispy form layout +- Enhance weekly time series default column name + ## [0.6.8] -- Add report_title to context +- Add report_title to context - Enhance SearchForm to be easier to override. Still needs more enhancements. - ## [0.6.7] -- Fix issue with `ReportField` when it has a `requires` in time series and crosstab reports +- Fix issue with `ReportField` when it has a `requires` in time series and crosstab reports ## [0.6.6] - Now a method on a generator can be effectively used as column - Use correct model when traversing on group by - ## [0.6.5] -- Fix Issue with group_by field pointing to model with custom primary key Issue #58 +- Fix Issue with group_by field pointing to model with custom primary key Issue #58 ## [0.6.4] + - Fix highchart cache to target the specific chart - Added initial and required to report_form_factory - Added base_q_filters and base_kwargs_filters to SlickReportField to control the base queryset -- Add ability to customize ReportField on the fly -- Adds `prevent_group_by` option to SlickReportField Will prevent group by calculation for this specific field, serves when you want to compute overall results. +- Add ability to customize ReportField on the fly +- Adds `prevent_group_by` option to SlickReportField Will prevent group by calculation for this specific field, serves + when you want to compute overall results. - Support reference to SlickReportField class directly in `requires` instead of its "registered" name. -- Adds PercentageToBalance report field +- Adds PercentageToBalance report field ## [0.6.3] -- Change the deprecated in Django 4 `request.is_ajax` . +- Change the deprecated in Django 4 `request.is_ajax` . ## [0.6.2] @@ -47,14 +57,13 @@ All notable changes to this project will be documented in this file. - Breaking [ONLY] if you have overridden ReportView.get_report_results() - Moved the collecting of total report data to the report generator to make easier low level usage. -- Fixed an issue with Charts.js `get_row_data` +- Fixed an issue with Charts.js `get_row_data` - Added ChartsOption 'time_series_support',in both chart.js and highcharts -- Fixed `SlickReportField.create` to use the issuing class not the vanilla one. - +- Fixed `SlickReportField.create` to use the issuing class not the vanilla one. ## [0.5.8] -- Fix compatibility with Django 3.2 +- Fix compatibility with Django 3.2 ## [0.5.7] @@ -63,14 +72,13 @@ All notable changes to this project will be documented in this file. ## [0.5.6] - Add exclude_field to report_form_factory (@gr4n0t4) -- Added support for group by Many To Many field (@gr4n0t4) +- Added support for group by Many To Many field (@gr4n0t4) ## [0.5.5] - Add datepicker initialization function call (@squio) - Fixed an issue with default dates not being functional. - ## [0.5.4] - Added missing prefix on integrity hash (@squio) @@ -78,7 +86,7 @@ All notable changes to this project will be documented in this file. ## [0.5.3] - Enhanced Field prepare flow -- Add traversing for group_by +- Add traversing for group_by - Allowed tests to run specific tests instead of the whole suit - Enhanced templates structure for easier override/customization @@ -88,7 +96,6 @@ All notable changes to this project will be documented in this file. - Enhanced the default verbose names of time series. - Expanding test coverage - ## [0.5.1] - Allow for time series to operate on a non-group by report diff --git a/slick_reporting/views.py b/slick_reporting/views.py index a87bdbf..4574fc7 100644 --- a/slick_reporting/views.py +++ b/slick_reporting/views.py @@ -300,10 +300,6 @@ def get_form_crispy_helper(self): def get_report_generator(self, queryset, for_print): q_filters, kw_filters = self.get_form_filters(self.form) - crosstab_compute_reminder = False - - time_series_pattern = self.time_series_pattern - return self.report_generator_class(self.get_report_model(), # start_date=self.form.cleaned_data['start_date'], # end_date=self.form.cleaned_data['end_date'], diff --git a/tests/test_generator.py b/tests/test_generator.py index 6df8a5b..f55d3f6 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -154,7 +154,6 @@ def load(): self.assertRaises(Exception, load) - @skip('Maybe we should not raise error on missing dates ') def test_missing_date_field(self): def load(): ReportGenerator(report_model=OrderLine, group_by='product', date_field='') From 1af33f23dd3ce50b820e5507f9027a31ae2cb0f6 Mon Sep 17 00:00:00 2001 From: Ramez Ashraf Date: Mon, 1 May 2023 00:30:05 +0300 Subject: [PATCH 09/20] ExportToStreamingCSV & ExportToCSV --- CHANGELOG.md | 1 + slick_reporting/views.py | 62 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b09670e..2a9dc23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. - Added SlickReportingListView - Added `show_time_series_selector` capability to SlickReportView allowing User to change the time series pattern from the UI. +- Added ability to export to CSV from UI, using `ExportToStreamingCSV` & `ExportToCSV` - Now you can have a custom column defined on the SlickReportView and not needing to customise the report generator. - You don't need to set date_field if you have calculations on the report - Easier customization of the crispy form layout diff --git a/slick_reporting/views.py b/slick_reporting/views.py index 4574fc7..9a2e537 100644 --- a/slick_reporting/views.py +++ b/slick_reporting/views.py @@ -1,10 +1,11 @@ import datetime +import csv import simplejson as json from django import forms from django.conf import settings from django.forms import modelform_factory -from django.http import HttpResponse +from django.http import HttpResponse, StreamingHttpResponse from django.utils.encoding import force_str from django.utils.functional import Promise from django.views.generic import FormView @@ -15,6 +16,55 @@ from .generator import ReportGenerator, ListViewReportGenerator +class ExportToCSV(object): + + def get_filename(self): + return self.report_title + + def get_response(self): + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename={filename}.csv'.format(filename=self.get_filename()) + + writer = csv.writer(response) + for rows in self.get_rows(): + writer.writerow(rows) + + return response + + def get_rows(self): + columns, verbose_names = self.get_columns() + yield verbose_names + for line in self.report_data['data']: + yield [line[col_name] for col_name in columns] + + def get_columns(self, extra_context=None): + return list(zip(*[(x['name'], x['verbose_name']) for x in self.report_data['columns']])) + + def __init__(self, request, report_data, report_title, **kwargs): + self.request = request + self.report_data = report_data + self.report_title = report_title + self.kwargs = kwargs + + +class ExportToStreamingCSV(ExportToCSV): + + def get_response(self): + # Copied form Djagno Docs + class Echo: + def write(self, value): + return value + + pseudo_buffer = Echo() + writer = csv.writer(pseudo_buffer) + return StreamingHttpResponse( + (writer.writerow(row) for row in self.get_rows()), + content_type="text/csv", + headers={ + "Content-Disposition": 'attachment; filename="{filename}.csv"'.format(filename=self.get_filename())} + ) + + class SlickReportViewBase(FormView): group_by = None columns = None @@ -50,6 +100,8 @@ class SlickReportViewBase(FormView): time_series_selector_default = None time_series_selector_allow_empty = False + csv_export_class = ExportToStreamingCSV + """ A list of chart settings objects instructing front end on how to plot the data. @@ -62,6 +114,11 @@ def get(self, request, *args, **kwargs): self.form = self.get_form(form_class) if self.form.is_valid(): report_data = self.get_report_results() + + export_csv = request.GET.get('csv', False) + if export_csv: + return self.export_csv(report_data) + if request.headers.get('x-requested-with') == 'XMLHttpRequest': return self.ajax_render_to_response(report_data) @@ -69,6 +126,9 @@ def get(self, request, *args, **kwargs): return self.render_to_response(self.get_context_data()) + def export_csv(self, report_data): + return self.csv_export_class(self.request, report_data, self.report_title).get_response() + @classmethod def get_report_model(cls): return cls.report_model or cls.queryset.model From 571602f75648b8c0ad6ea6ca610bbb8537f47958 Mon Sep 17 00:00:00 2001 From: Ramez Ashraf Date: Wed, 3 May 2023 13:36:59 +0300 Subject: [PATCH 10/20] Export Option --- docs/source/index.rst | 11 ++++++-- docs/source/the_view.rst | 25 +++++++++++++++++- .../templates/slick_reporting/base.html | 10 +++++++ .../slick_reporting/simple_report.html | 14 ++++++++-- slick_reporting/views.py | 26 ++++++++----------- 5 files changed, 66 insertions(+), 20 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 155902d..eb8d6f2 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -11,8 +11,7 @@ To install django-slick-reporting: 1. Install with pip: `pip install django-slick-reporting`. 2. Add ``slick_reporting`` to ``INSTALLED_APPS``. -3. For the shipped in View, add ``'crispy_forms'`` to ``INSTALLED_APPS`` and add ``CRISPY_TEMPLATE_PACK = 'bootstrap4'`` - to your ``settings.py`` +3. For the shipped in View, add ``'crispy_forms'`` to ``INSTALLED_APPS`` and add ``CRISPY_TEMPLATE_PACK = 'bootstrap4'`` to your ``settings.py`` 4. Execute `python manage.py collectstatic` so the JS helpers are collected and served. Demo site @@ -20,6 +19,14 @@ Demo site https://django-slick-reporting.com is a quick walk-though with live code examples +Options +------- +* Compute different types of fields (Sum, Avg, Count, Min, Max, StdDev, Variance) on a model +* Group by a foreign key, date, or any other field +* Display the results in a table +* Display the results in a chart (Highcharts or Charts.js) +* Export the results to CSV , extendable easily + Quickstart ---------- diff --git a/docs/source/the_view.rst b/docs/source/the_view.rst index c972474..76e8695 100644 --- a/docs/source/the_view.rst +++ b/docs/source/the_view.rst @@ -7,17 +7,40 @@ What is SlickReportView? ----------------------- SlickReportView is a CBV that inherits form ``FromView`` and expose the report generator needed attributes. -it also +Also * Auto generate the search form * return the results as a json response if ajax request * Works on GET and POST +* Export to CSV (extendable to apply other exporting method) + How the search form is generated ? ----------------------------------- Behind the scene, Sample report calls ``slick_reporting.form_factory.report_form_factory`` a helper method which generates a form containing start date and end date, as well as all foreign keys on the report_model. + +Export to CSV +-------------- +To trigger an export to CSV, just add ``?_export=csv`` to the url. +This will call the export_csv on the view class, engaging a `ExportToStreamingCSV` + +You can extend the functionality, say you want to export to pdf. +Add a ``export_pdf`` method to the view class, accepting the report_data json response and return the response you want. +This ``export_pdf` will be called automatically when url parameter contain ``?_export=pdf`` + +Having an `_export` parameter not implemented, to say the view class do not implement ``export_{parameter_name}``, will be ignored. + +SlickReportingListView +----------------------- +This is a simple ListView to display data in a model, like you would with an admin ChangeList view. +It's a simple ListView with a few extra features: + +filters: a list of report_model fields to be used as filters. + + + Override the Form ------------------ diff --git a/slick_reporting/templates/slick_reporting/base.html b/slick_reporting/templates/slick_reporting/base.html index 235f807..f1e5109 100644 --- a/slick_reporting/templates/slick_reporting/base.html +++ b/slick_reporting/templates/slick_reporting/base.html @@ -37,14 +37,24 @@ +{#select2#} + + + +{# datatable #} + + + + + {% block js_script %} {% endblock %} diff --git a/slick_reporting/templates/slick_reporting/simple_report.html b/slick_reporting/templates/slick_reporting/simple_report.html index 3a65ae1..8f82671 100644 --- a/slick_reporting/templates/slick_reporting/simple_report.html +++ b/slick_reporting/templates/slick_reporting/simple_report.html @@ -6,9 +6,10 @@ {% if form %}

Filters

-
- {% crispy form form.get_crispy_helper %} + + {% crispy form crispy_helper %} +
{% endif %}

Results

@@ -64,6 +65,15 @@

Results

let chartContainer = $('.reportChart') + $('.exportCsvBtn').on('click', function (e) { + e.preventDefault() + let form = $('#reportForm'); + window.location = '?' + form.serialize()+ '&_export=csv' + {#form.attr('action', '{% url 'slick_reporting:export_csv' %}');#} + {#form.submit();#} + }); + + function setDatePicker() { function setDatePickerObj() { var range_start = new Date(); diff --git a/slick_reporting/views.py b/slick_reporting/views.py index 9a2e537..34c76c0 100644 --- a/slick_reporting/views.py +++ b/slick_reporting/views.py @@ -115,9 +115,12 @@ def get(self, request, *args, **kwargs): if self.form.is_valid(): report_data = self.get_report_results() - export_csv = request.GET.get('csv', False) - if export_csv: - return self.export_csv(report_data) + export_option = request.GET.get('_export', '') + if export_option: + try: + return getattr(self, f'export_{export_option}')(report_data) + except AttributeError: + pass if request.headers.get('x-requested-with') == 'XMLHttpRequest': return self.ajax_render_to_response(report_data) @@ -346,7 +349,11 @@ def get_form_filters(self, form): elif type(field) is forms.BooleanField: # boolean field while checked on frontend , and have initial = True, give false value on cleaned_data # Hence this check to see if it was indeed in the GET params, - kw_filters[name] = form.cleaned_data[name] if name in self.request.GET else field.initial + value = field.initial + if self.request.GET: + value = form.cleaned_data.get(name, False) + kw_filters[name] = value + else: value = form.cleaned_data[name] if value: @@ -381,17 +388,6 @@ def get_form_class(self): return modelform_factory(self.get_report_model(), fields=self.filters, formfield_callback=default_formfield_callback) - def get(self, request, *args, **kwargs): - form_class = self.get_form_class() - self.form = self.get_form(form_class) - if self.form.is_valid(): - report_data = self.get_report_results() - if request.headers.get('x-requested-with') == 'XMLHttpRequest': - return self.ajax_render_to_response(report_data) - - return self.render_to_response(self.get_context_data(report_data=report_data)) - - return self.render_to_response(self.get_context_data()) def get_report_results(self, for_print=False): """ From e41a7a4acb747ebb97d45dd33c7bee455dfc9dcd Mon Sep 17 00:00:00 2001 From: Ramez Ashraf Date: Sun, 7 May 2023 16:04:55 +0300 Subject: [PATCH 11/20] change in default template --- .../templates/slick_reporting/base.html | 9 +- .../slick_reporting/simple_report.html | 112 ++++++++++-------- 2 files changed, 63 insertions(+), 58 deletions(-) diff --git a/slick_reporting/templates/slick_reporting/base.html b/slick_reporting/templates/slick_reporting/base.html index f1e5109..c3de1c9 100644 --- a/slick_reporting/templates/slick_reporting/base.html +++ b/slick_reporting/templates/slick_reporting/base.html @@ -39,15 +39,8 @@ {#select2#} - - - {# datatable #} @@ -55,7 +48,7 @@ -{% block js_script %} +{% block extrajs %} {% endblock %} \ No newline at end of file diff --git a/slick_reporting/templates/slick_reporting/simple_report.html b/slick_reporting/templates/slick_reporting/simple_report.html index 8f82671..d4a8c09 100644 --- a/slick_reporting/templates/slick_reporting/simple_report.html +++ b/slick_reporting/templates/slick_reporting/simple_report.html @@ -4,74 +4,86 @@ {% block content %} -{% if form %} -

Filters

-
- {% crispy form crispy_helper %} - - -
-{% endif %} -

Results

-
-
-
- -
- {% if report_data.chart_settings %} - {# #} - {% if report_data.charts_engine == 'chartsjs' %} - - {% elif report_data.charts_engine == 'highcharts' %} - {#
#} - {% endif %} +
+ {% if form %} + +

Filters

+
+ {% crispy form crispy_helper %} + + +
+ {% endif %} +

Results

+
+
+
+ +
+ {% if report_data.chart_settings %} + {# #} + {% if report_data.charts_engine == 'chartsjs' %} + + {% elif report_data.charts_engine == 'highcharts' %} + {#
#} + {% endif %} - {% endif %} -
-
- {% include 'slick_reporting/table.html' with table=report_data %} + {% endif %} +
+
+ {% include 'slick_reporting/table.html' with table=report_data %} +
-
+
{% endblock %} -{% block js_script %} +{% block extrajs %} + {{ block.super }} + + +