From 857162672776bea941622807b88bdf9ea4767839 Mon Sep 17 00:00:00 2001 From: Ramez Ashraf Date: Mon, 29 May 2023 18:07:07 +0300 Subject: [PATCH 01/28] - Allow cross tab on fields & deprecate `crosstab_model` in favor of crosstab_field, to be removed next version. - Add support for start_date_field_name and end_date_field_name --- CHANGELOG.md | 5 +++ slick_reporting/generator.py | 60 ++++++++++++++++++++++++++---------- tests/report_generators.py | 18 +++++++++++ tests/test_generator.py | 12 +++++++- tests/tests.py | 23 +++++++++++++- 5 files changed, 100 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4168d9d..79a00b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,16 @@ All notable changes to this project will be documented in this file. +## [unreleased] +- Allow cross tab on fields & deprecate `crosstab_model` in favor of crosstab_field, to be removed next version. +- Add support for start_date_field_name and end_date_field_name + ## [0.8.0] - Breaking: [Only if you use Crosstab reports] renamed crosstab_compute_reminder to crosstab_compute_remainder - Breaking : [Only if you set the templates statics by hand] renamed slick_reporting to ra.hightchart.js and ra.chartjs.js to erp_framework.highchart.js and erp_framework.chartjs.js respectively +- Fix an issue with Crosstab when there crosstab_compute_remainder = False ## [0.7.0] diff --git a/slick_reporting/generator.py b/slick_reporting/generator.py index 6e03c1f..c528c41 100644 --- a/slick_reporting/generator.py +++ b/slick_reporting/generator.py @@ -2,6 +2,7 @@ import datetime import logging +from warnings import warn from dataclasses import dataclass from inspect import isclass @@ -59,6 +60,12 @@ class ReportGenerator(object): date_field = None """Main date field to use whenever date filter is needed""" + start_date_field_name = None + """If set, the report will use this field to filter the start date, default to date_field""" + + end_date_field_name = None + """If set, the report will use this field to filter the end date, default to date_field""" + print_flag = None list_display_links = [] @@ -112,10 +119,13 @@ class ReportGenerator(object): Example: [ (start_date_1, end_date_1), (start_date_2, end_date_2), ....] """ - crosstab_model = None + crosstab_model = None # deprecated + + crosstab_field = None """ If set, a cross tab over this model selected ids (via `crosstab_ids`) """ + crosstab_columns = None """The computation fields which will be computed for each crosstab-ed ids """ @@ -159,6 +169,7 @@ def __init__( time_series_columns=None, time_series_custom_dates=None, crosstab_model=None, + crosstab_field=None, crosstab_columns=None, crosstab_ids=None, crosstab_compute_remainder=None, @@ -170,6 +181,8 @@ def __init__( limit_records=False, format_row_func=None, container_class=None, + start_date_field_name=None, + end_date_field_name=None, ): """ @@ -220,10 +233,25 @@ def __init__( ) self.date_field = self.date_field or date_field + self.start_date_field_name = ( + self.start_date_field_name or start_date_field_name or self.date_field + ) + self.end_date_field_name = ( + self.end_date_field_name or end_date_field_name or self.date_field + ) + self.q_filters = q_filters or [] self.kwargs_filters = kwargs_filters or {} + self.crosstab_field = self.crosstab_field or crosstab_field self.crosstab_model = self.crosstab_model or crosstab_model + if self.crosstab_model: + warn( + "crosstab_model is deprecated; use crosstab_field instead", + DeprecationWarning, + ) + self.crosstab_field = self.crosstab_field or self.crosstab_model + self.crosstab_columns = crosstab_columns or self.crosstab_columns or [] self.crosstab_ids = self.crosstab_ids or crosstab_ids or [] self.crosstab_compute_remainder = ( @@ -247,11 +275,11 @@ def __init__( ) self.container_class = container_class - if not self.date_field and ( - self.time_series_pattern or self.crosstab_model or self.group_by - ): + if not ( + self.date_field or (self.start_date_field_name and self.end_date_field_name) + ) and (self.time_series_pattern or self.crosstab_field or self.group_by): raise ImproperlyConfigured( - "date_field must be set on a class level or via init" + f"date_field or [start_date_field_name and end_date_field_name] must be set for {self}" ) self._prepared_results = {} @@ -298,9 +326,6 @@ def __init__( self.swap_sign = self.swap_sign or swap_sign self.limit_records = self.limit_records or limit_records - # passed to the report fields - # self.date_field = date_field or self.date_field - # in case of a group by, do we show a grouped by model data regardless of their appearance in the results # a client who didn't make a transaction during the date period. self.show_empty_records = False # show_empty_records if show_empty_records else self.show_empty_records @@ -361,8 +386,8 @@ def _apply_queryset_options(self, query, fields=None): filters = {} if self.date_field: filters = { - f"{self.date_field}__gt": self.start_date, - f"{self.date_field}__lte": self.end_date, + f"{self.start_date_field_name}__gt": self.start_date, + f"{self.end_date_field_name}__lte": self.end_date, } filters.update(self.kwargs_filters) @@ -378,10 +403,12 @@ def _construct_crosstab_filter(self, col_data): :param col_data: :return: """ + field = get_field_from_query_text(col_data["crosstab_field"], self.report_model) + column_name = field.column if col_data["is_remainder"]: - filters = [~Q(**{f"{col_data['model']}_id__in": self.crosstab_ids})] + filters = [~Q(**{f"{column_name}__in": self.crosstab_ids})] else: - filters = [Q(**{f"{col_data['model']}_id": col_data["id"]})] + filters = [Q(**{f"{column_name}": col_data["id"]})] return filters def _prepare_report_dependencies(self): @@ -685,7 +712,7 @@ def get_list_display_columns(self): except ValueError: columns += time_series_columns - if self.crosstab_model: + if self.crosstab_field: crosstab_columns = self.get_crosstab_parsed_columns() try: @@ -822,11 +849,12 @@ def get_crosstab_parsed_columns(self): "name": f"{magic_field_class.name}CT{id}", "original_name": magic_field_class.name, "verbose_name": self.get_crosstab_field_verbose_name( - magic_field_class, self.crosstab_model, id + magic_field_class, self.crosstab_field, id ), "ref": magic_field_class, "id": id, - "model": self.crosstab_model, + "crosstab_field": self.crosstab_field, + # "model": self.crosstab_model, "is_remainder": counter == ids_length if self.crosstab_compute_remainder else False, @@ -860,7 +888,7 @@ def get_metadata(self): "time_series_column_verbose_names": [ x["verbose_name"] for x in time_series_columns ], - "crosstab_model": self.crosstab_model or "", + "crosstab_model": self.crosstab_field or "", "crosstab_column_names": [x["name"] for x in crosstab_columns], "crosstab_column_verbose_names": [ x["verbose_name"] for x in crosstab_columns diff --git a/tests/report_generators.py b/tests/report_generators.py index 8962ef9..9e17a0f 100644 --- a/tests/report_generators.py +++ b/tests/report_generators.py @@ -11,6 +11,7 @@ Product, SalesWithFlag, SalesProductWithCustomID, + ComplexSales, ) from .models import OrderLine @@ -49,6 +50,23 @@ class CrosstabOnClient(GenericGenerator): ] +class CrosstabOnField(ReportGenerator): + report_model = ComplexSales + date_field = "doc_date" + + group_by = "product" + columns = ["name"] + crosstab_model = "flag" + crosstab_field = "flag" + crosstab_ids = ["sales", "sales-return"] + + crosstab_columns = [ + SlickReportField.create( + Sum, "quantity", name="value__sum", verbose_name=_("Sales") + ) + ] + + # diff --git a/tests/test_generator.py b/tests/test_generator.py index b49b00c..ac6b7b8 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -15,13 +15,14 @@ GenericGenerator, GroupByCharField, TimeSeriesCustomDates, + CrosstabOnField, ) from .tests import BaseTestData, year from .models import SimpleSales, Client -class MatrixTests(BaseTestData, TestCase): +class CrosstabTests(BaseTestData, TestCase): def test_matrix_column_included(self): report = CrosstabOnClient( crosstab_ids=[self.client1.pk], crosstab_compute_remainder=False @@ -75,6 +76,15 @@ def test_get_crosstab_parsed_columns(self): for col in columns: self.assertTrue("is_summable" in col.keys(), col) + def test_crosstab_on_field(self): + report = CrosstabOnField() + data = report.get_report_data() + self.assertEqual(len(data), 2, data) + self.assertEqual(data[0]["value__sumCTsales"], 90, data) + self.assertEqual(data[0]["value__sumCTsales-return"], 30, data) + self.assertEqual(data[0]["value__sumCT----"], 77, data) + self.assertEqual(data[1]["value__sumCTsales-return"], 34, data) + class GeneratorReportStructureTest(BaseTestData, TestCase): @classmethod diff --git a/tests/tests.py b/tests/tests.py index 607e307..e0895ee 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -166,6 +166,7 @@ def setUpTestData(cls): product=cls.product1, quantity=30, price=10, + flag="sales", ) sale2 = ComplexSales.objects.create( doc_date=datetime.datetime(year, 3, 2), @@ -173,6 +174,7 @@ def setUpTestData(cls): product=cls.product1, quantity=30, price=10, + flag="sales", ) sale3 = ComplexSales.objects.create( doc_date=datetime.datetime(year, 3, 2), @@ -180,6 +182,7 @@ def setUpTestData(cls): product=cls.product1, quantity=30, price=10, + flag="sales", ) sale4 = ComplexSales.objects.create( doc_date=datetime.datetime(year, 3, 2), @@ -187,6 +190,23 @@ def setUpTestData(cls): product=cls.product1, quantity=30, price=10, + flag="sales-return", + ) + sale4 = ComplexSales.objects.create( + doc_date=datetime.datetime(year, 3, 2), + client=cls.client3, + product=cls.product2, + quantity=34, + price=10, + flag="sales-return", + ) + sale5 = ComplexSales.objects.create( + doc_date=datetime.datetime(year, 3, 2), + client=cls.client2, + product=cls.product1, + quantity=77, + price=10, + flag="", ) sale1.tax.add(cls.tax1) sale1.tax.add(cls.tax2) @@ -362,7 +382,8 @@ def test_many_to_many_group_by(self): columns=["tax__name", "tax__count"], ) data = report_generator.get_report_data() - self.assertEqual(len(data), 3) + + self.assertEqual(len(data), 4) # 3 taxes + 1 empty self.assertEqual(data[0]["tax__name"], "State") self.assertEqual(data[0]["tax__count"], 3) self.assertEqual(data[1]["tax__name"], "Vat reduced") From 0129181e1ee65b02ed505e6c4bd0e49afe642ae5 Mon Sep 17 00:00:00 2001 From: Ramez Ashraf Date: Tue, 30 May 2023 14:06:22 +0300 Subject: [PATCH 02/28] WIP --- slick_reporting/generator.py | 8 +- tests/models.py | 33 ++++++- tests/report_generators.py | 38 ++++++++ tests/tests.py | 164 +++++++++++++++++++++++++++++++++++ tests/urls.py | 20 +++++ tests/views.py | 84 +++++++++++++++++- 6 files changed, 343 insertions(+), 4 deletions(-) diff --git a/slick_reporting/generator.py b/slick_reporting/generator.py index c528c41..676f446 100644 --- a/slick_reporting/generator.py +++ b/slick_reporting/generator.py @@ -345,6 +345,7 @@ def __init__( else: self.main_queryset = self._apply_queryset_options(main_queryset) + # breakpoint() if type(self.group_by_field) is ForeignKey: ids = self.main_queryset.values_list( self.group_by_field_attname @@ -360,7 +361,7 @@ def __init__( ) self.main_queryset = ( self.group_by_field.related_model.objects.filter( - pk__in=ids + **{f"{self.group_by_field.target_field.name}__in": ids} ).values(*final_fields) ) else: @@ -404,7 +405,10 @@ def _construct_crosstab_filter(self, col_data): :return: """ field = get_field_from_query_text(col_data["crosstab_field"], self.report_model) - column_name = field.column + if field is ForeignKey: + column_name = field.target_field.name + else: + column_name = field.column if col_data["is_remainder"]: filters = [~Q(**{f"{column_name}__in": self.crosstab_ids})] else: diff --git a/tests/models.py b/tests/models.py index 0f9339a..2894675 100644 --- a/tests/models.py +++ b/tests/models.py @@ -57,7 +57,7 @@ class Contact(models.Model): class Client(models.Model): slug = models.CharField(max_length=200, verbose_name=_("Client Slug")) - name = models.CharField(max_length=200, verbose_name=_("Name")) + name = models.CharField(max_length=200, verbose_name=_("Name"), unique=True) email = models.EmailField(blank=True) notes = models.TextField() contact = models.ForeignKey(Contact, on_delete=models.CASCADE, null=True) @@ -98,6 +98,37 @@ class Meta: ordering = ["-created_at"] +class SimpleSales2(models.Model): + slug = models.SlugField() + doc_date = models.DateTimeField(_("date"), db_index=True) + client = models.ForeignKey(Client, on_delete=models.CASCADE, to_field="name") + product = models.ForeignKey(Product, on_delete=models.CASCADE) + quantity = models.DecimalField( + _("quantity"), max_digits=19, decimal_places=2, default=0 + ) + price = models.DecimalField(_("price"), max_digits=19, decimal_places=2, default=0) + value = models.DecimalField(_("value"), max_digits=19, decimal_places=2, default=0) + created_at = models.DateTimeField(null=True, verbose_name=_("Created at")) + flag = models.CharField(max_length=50, default="sales") + + content_type = models.ForeignKey( + ContentType, on_delete=models.DO_NOTHING, null=True + ) + object_id = models.PositiveIntegerField(null=True) + content_object = GenericForeignKey("content_type", "object_id") + + def save( + self, force_insert=False, force_update=False, using=None, update_fields=None + ): + self.value = self.quantity * self.price + super().save(force_insert, force_update, using, update_fields) + + class Meta: + verbose_name = _("Sale") + verbose_name_plural = _("Sales") + ordering = ["-created_at"] + + class SalesProductWithCustomID(models.Model): slug = models.SlugField() doc_date = models.DateTimeField(_("date"), db_index=True) diff --git a/tests/report_generators.py b/tests/report_generators.py index 9e17a0f..4dfca20 100644 --- a/tests/report_generators.py +++ b/tests/report_generators.py @@ -12,6 +12,7 @@ SalesWithFlag, SalesProductWithCustomID, ComplexSales, + SimpleSales2, ) from .models import OrderLine @@ -77,6 +78,13 @@ class ClientTotalBalance(ReportGenerator): columns = ["slug", "name", "__balance__", "__total__"] +class ClientTotalBalance2(ReportGenerator): + report_model = SimpleSales2 + date_field = "doc_date" + group_by = "client" + columns = ["slug", "name", "__balance__", "__total__"] + + class GroupByCharField(ReportGenerator): report_model = SalesWithFlag date_field = "doc_date" @@ -303,6 +311,17 @@ class ProductClientSalesMatrix(ReportGenerator): crosstab_columns = ["__total__"] +class ProductClientSalesMatrixToFieldSet(ReportGenerator): + report_model = SimpleSales2 + date_field = "doc_date" + + group_by = "product" + columns = ["slug", "name"] + + crosstab_model = "client" + crosstab_columns = ["__total__"] + + class ProductClientSalesMatrix2(ReportGenerator): report_model = SimpleSales date_field = "doc_date" @@ -318,6 +337,25 @@ class ProductClientSalesMatrix2(ReportGenerator): ] +class ProductClientSalesMatrixwSimpleSales2(ReportGenerator): + report_model = SimpleSales2 + date_field = "doc_date" + + group_by = "product" + columns = ["slug", "name"] + + crosstab_model = "client" + crosstab_columns = [ + SlickReportField.create( + Sum, "value", name="value__sum", verbose_name=_("Sales") + ) + ] + + +class GeneratorClassWithAttrsAs(ReportGenerator): + columns = ["get_icon", "slug", "name"] + + class ClientTotalBalancesWithShowEmptyFalse(ClientTotalBalance): report_slug = None default_order_by = "-__balance__" diff --git a/tests/tests.py b/tests/tests.py index e0895ee..db7ece0 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -15,6 +15,7 @@ GroupByCharField, GroupByCharFieldPlusTimeSeries, TimeSeriesWithOutGroupBy, + ProductClientSalesMatrixwSimpleSales2, ) from . import report_generators from .models import ( @@ -29,6 +30,7 @@ ProductCustomID, SalesProductWithCustomID, Agent, + SimpleSales2, ) from .views import SlickReportView @@ -156,6 +158,80 @@ def setUpTestData(cls): price=10, ) + SimpleSales2.objects.create( + doc_date=datetime.datetime(year, 1, 2), + client=cls.client1, + product=cls.product1, + quantity=10, + price=10, + created_at=datetime.datetime(year, 1, 5), + ) + SimpleSales2.objects.create( + doc_date=datetime.datetime(year, 2, 2), + client=cls.client1, + product=cls.product1, + quantity=10, + price=10, + created_at=datetime.datetime(year, 2, 3), + ) + + SimpleSales2.objects.create( + doc_date=datetime.datetime(year, 3, 2), + client=cls.client1, + product=cls.product1, + quantity=10, + price=10, + created_at=datetime.datetime(year, 3, 3), + ) + + # client 2 + SimpleSales2.objects.create( + doc_date=datetime.datetime(year, 1, 2), + client=cls.client2, + product=cls.product1, + quantity=20, + price=10, + ) + SimpleSales2.objects.create( + doc_date=datetime.datetime(year, 2, 2), + client=cls.client2, + product=cls.product1, + quantity=20, + price=10, + ) + + SimpleSales2.objects.create( + doc_date=datetime.datetime(year, 3, 2), + client=cls.client2, + product=cls.product1, + quantity=20, + price=10, + ) + + # client 3 + SimpleSales2.objects.create( + doc_date=datetime.datetime(year, 1, 2), + client=cls.client3, + product=cls.product1, + quantity=30, + price=10, + ) + SimpleSales2.objects.create( + doc_date=datetime.datetime(year, 2, 2), + client=cls.client3, + product=cls.product1, + quantity=30, + price=10, + ) + + SimpleSales2.objects.create( + doc_date=datetime.datetime(year, 3, 2), + client=cls.client3, + product=cls.product1, + quantity=30, + price=10, + ) + cls.tax1 = TaxCode.objects.create(name="State", tax=8) # Added three times cls.tax2 = TaxCode.objects.create(name="Vat reduced", tax=5) # Added two times cls.tax3 = TaxCode.objects.create(name="Vat full", tax=20) # Added one time @@ -352,6 +428,29 @@ def test_filters(self): data = report.get_report_data() self.assertEqual(len(data), 1, data) + def test_view_filter_to_field_set(self): + report_generator = ReportGenerator( + report_model=SimpleSales2, + date_field="doc_date", + group_by="client", + columns=["slug", "name"], + time_series_pattern="monthly", + time_series_columns=["__total__", "__balance__"], + ) + data = report_generator.get_report_data() + response = self.client.get( + reverse("report-to-field-set"), + data={ + "client_id": [self.client2.name, self.client1.name], + }, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assertEqual(response.status_code, 200) + + view_report_data = response.json() + self.assertTrue(len(data), 2) + # self.assertEqual(view_report_data['data'], data) + def test_filter_as_int_n_list(self): report = ClientTotalBalance( kwargs_filters={"client": self.client1.pk}, show_empty_records=True @@ -445,6 +544,31 @@ def test_view_filter(self): self.assertTrue(len(data), 2) # self.assertEqual(view_report_data['data'], data) + def test_view_filter_to_field_set(self): + report_generator = ReportGenerator( + report_model=SimpleSales2, + date_field="doc_date", + group_by="client", + columns=["slug", "name"], + # time_series_pattern="monthly", + # time_series_columns=["__total__", "__balance__"], + ) + data = report_generator.get_report_data() + response = self.client.get( + reverse("report-to-field-set"), + # data={ + # "client_id": [self.client2.name, self.client1.name], + # }, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assertEqual(response.status_code, 200) + # breakpoint() + self.assertTrue(len(data), 2) + + view_report_data = response.json() + + # self.assertEqual(view_report_data['data'], data) + def test_ajax(self): report_generator = ReportGenerator( report_model=SimpleSales, @@ -502,6 +626,46 @@ def test_crosstab_report_view_clumns_on_fly(self): view_report_data = response.json() self.assertEqual(view_report_data["data"], data, view_report_data) + def test_crosstab_report_view_to_field_set(self): + from .report_generators import ProductClientSalesMatrixToFieldSet + + data = ProductClientSalesMatrixToFieldSet( + crosstab_compute_remainder=True, + crosstab_ids=[self.client1.name, self.client2.name], + ).get_report_data() + + response = self.client.get(reverse("product_crosstab_client_to_field_set")) + self.assertEqual(response.status_code, 200) + response = self.client.get( + reverse("product_crosstab_client_to_field_set"), + data={ + "client_id": [self.client1.name, self.client2.name], + "crosstab_compute_remainder": True, + }, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assertEqual(response.status_code, 200) + view_report_data = response.json() + self.assertEqual(view_report_data["data"], data) + + def test_crosstab_report_view_clumns_on_fly_to_field_set(self): + data = ProductClientSalesMatrixwSimpleSales2( + crosstab_compute_remainder=True, + crosstab_ids=[self.client1.name, self.client2.name], + ).get_report_data() + + response = self.client.get( + reverse("crosstab-columns-on-fly-to-field-set"), + data={ + "client_id": [self.client1.name, self.client2.name], + "crosstab_compute_remainder": True, + }, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assertEqual(response.status_code, 200) + view_report_data = response.json() + self.assertEqual(view_report_data["data"], data, view_report_data) + def test_chart_settings(self): response = self.client.get( reverse("product_crosstab_client"), diff --git a/tests/urls.py b/tests/urls.py index f5e98f2..4cacd98 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -8,11 +8,31 @@ views.ProductClientSalesMatrix.as_view(), name="product_crosstab_client", ), + path( + "report-to-field-set/", + views.MonthlyProductSalesToFIeldSet.as_view(), + name="report-to-field-set", + ), + path( + "product_crosstab_client/", + views.ProductClientSalesMatrix.as_view(), + name="product_crosstab_client", + ), + path( + "product_crosstab_client-to_field-set/", + views.ProductClientSalesMatrixToFIeldSet.as_view(), + name="product_crosstab_client_to_field_set", + ), path( "crosstab-columns-on-fly/", views.CrossTabColumnOnFly.as_view(), name="crosstab-columns-on-fly", ), + path( + "crosstab-columns-on-fly-to-field-set/", + views.CrossTabColumnOnFlyToFieldSet.as_view(), + name="crosstab-columns-on-fly-to-field-set", + ), path( "queryset-only/", views.MonthlyProductSalesWQS.as_view(), name="queryset-only" ), diff --git a/tests/views.py b/tests/views.py index 22004e0..c55c0e9 100644 --- a/tests/views.py +++ b/tests/views.py @@ -1,7 +1,7 @@ from slick_reporting.views import SlickReportView from slick_reporting.fields import SlickReportField, TotalReportField from django.db.models import Sum, Count -from .models import SimpleSales, ComplexSales +from .models import SimpleSales, ComplexSales, SimpleSales2 from django.utils.translation import gettext_lazy as _ @@ -14,6 +14,15 @@ class MonthlyProductSales(SlickReportView): time_series_columns = ["__total__", "__balance__"] +class MonthlyProductSalesToFIeldSet(SlickReportView): + report_model = SimpleSales2 + date_field = "doc_date" + group_by = "client" + columns = ["slug", "name"] + time_series_pattern = "monthly" + time_series_columns = ["__total__", "__balance__"] + + class ProductClientSalesMatrix(SlickReportView): report_title = "awesome report title" report_model = SimpleSales @@ -34,6 +43,26 @@ class ProductClientSalesMatrix(SlickReportView): ] +class ProductClientSalesMatrixToFIeldSet(SlickReportView): + report_title = "awesome report title" + report_model = SimpleSales2 + date_field = "doc_date" + + group_by = "product" + columns = ["slug", "name"] + + crosstab_model = "client" + crosstab_columns = ["__total__"] + + chart_settings = [ + { + "type": "pie", + "date_source": "__total__", + "title_source": "__total__", + } + ] + + class CrossTabColumnOnFly(SlickReportView): report_title = "awesome report title" report_model = SimpleSales @@ -58,6 +87,30 @@ class CrossTabColumnOnFly(SlickReportView): ] +class CrossTabColumnOnFlyToFieldSet(SlickReportView): + report_title = "awesome report title" + report_model = SimpleSales2 + date_field = "doc_date" + + group_by = "product" + columns = ["slug", "name"] + + crosstab_model = "client" + crosstab_columns = [ + SlickReportField.create( + Sum, "value", name="value__sum", verbose_name=_("Sales") + ) + ] + + chart_settings = [ + { + "type": "pie", + "date_source": "value__sum", + "title_source": "name", + } + ] + + class MonthlyProductSalesWQS(SlickReportView): # report_model = SimpleSales queryset = SimpleSales.objects.all() @@ -86,3 +139,32 @@ class TaxSales(SlickReportView): "title_source": "tax__name", } ] + + +class MonthlyProductSalesToFIeldSet(SlickReportView): + report_model = SimpleSales2 + date_field = "doc_date" + group_by = "client" + columns = ["slug", "name"] + time_series_pattern = "monthly" + time_series_columns = ["__total__", "__balance__"] + + +class TaxSales(SlickReportView): + # report_model = SimpleSales + queryset = ComplexSales.objects.all() + date_field = "doc_date" + group_by = "tax__name" + columns = [ + "tax__name", + SlickReportField.create( + Count, "tax", name="tax__count", verbose_name=_("Sales") + ), + ] + chart_settings = [ + { + "type": "pie", + "date_source": "tax__count", + "title_source": "tax__name", + } + ] From aa4d14d09654b416d12e358ba561052f6e168608 Mon Sep 17 00:00:00 2001 From: Ramez Ashraf Date: Tue, 30 May 2023 14:56:56 +0300 Subject: [PATCH 03/28] Handle to_field in crosstab --- slick_reporting/form_factory.py | 10 +++++++++- slick_reporting/generator.py | 5 +---- tests/views.py | 1 + 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/slick_reporting/form_factory.py b/slick_reporting/form_factory.py index d4552f8..c661512 100644 --- a/slick_reporting/form_factory.py +++ b/slick_reporting/form_factory.py @@ -112,7 +112,9 @@ def get_crosstab_ids(self): """ if self.crosstab_model: qs = self.cleaned_data.get(self.crosstab_key_name) - return [x for x in qs.values_list("pk", flat=True)] + return [ + x for x in qs.values_list(self.crosstab_field_related_name, flat=True) + ] return [] def get_crosstab_compute_remainder(self): @@ -168,6 +170,7 @@ def report_form_factory( :param required a list of fields that should be marked as required :return: """ + crosstab_field_related_name = "" foreign_key_widget_func = foreign_key_widget_func or _default_foreign_key_widget fkeys_filter_func = fkeys_filter_func or (lambda x: x) @@ -223,6 +226,10 @@ def report_form_factory( fields["crosstab_compute_remainder"] = forms.BooleanField( required=False, label=_("Display the crosstab remainder"), initial=True ) + crosstab_field_klass = [ + x for x in model._meta.get_fields() if x.name == crosstab_model + ] + crosstab_field_related_name = crosstab_field_klass[0].to_fields[0] bases = ( BaseReportForm, @@ -237,6 +244,7 @@ def report_form_factory( "foreign_keys": fkeys_map, "crosstab_model": crosstab_model, "crosstab_display_compute_remainder": display_compute_remainder, + "crosstab_field_related_name": crosstab_field_related_name, }, ) return new_form diff --git a/slick_reporting/generator.py b/slick_reporting/generator.py index 676f446..a28d6f1 100644 --- a/slick_reporting/generator.py +++ b/slick_reporting/generator.py @@ -405,10 +405,7 @@ def _construct_crosstab_filter(self, col_data): :return: """ field = get_field_from_query_text(col_data["crosstab_field"], self.report_model) - if field is ForeignKey: - column_name = field.target_field.name - else: - column_name = field.column + column_name = field.column if col_data["is_remainder"]: filters = [~Q(**{f"{column_name}__in": self.crosstab_ids})] else: diff --git a/tests/views.py b/tests/views.py index c55c0e9..5c76a13 100644 --- a/tests/views.py +++ b/tests/views.py @@ -52,6 +52,7 @@ class ProductClientSalesMatrixToFIeldSet(SlickReportView): columns = ["slug", "name"] crosstab_model = "client" + crosstab_field = "client" crosstab_columns = ["__total__"] chart_settings = [ From 1e115ca0cd399fb97a42141ce056899bf21f0fba Mon Sep 17 00:00:00 2001 From: Ramez Ashraf Date: Tue, 30 May 2023 15:51:45 +0300 Subject: [PATCH 04/28] Fixes --- docs/source/conf.py | 25 +++++++++++++------------ runtests.py | 14 ++++++++------ tests/models.py | 6 ++++++ tests/{test_settings.py => settings.py} | 0 tests/urls.py | 2 +- tests/views.py | 2 +- 6 files changed, 29 insertions(+), 20 deletions(-) rename tests/{test_settings.py => settings.py} (100%) diff --git a/docs/source/conf.py b/docs/source/conf.py index 305a80b..eabe897 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,20 +14,20 @@ import sys import django -sys.path.insert(0, os.path.abspath('../../')) -os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.test_settings' +sys.path.insert(0, os.path.abspath("../../")) +os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings" django.setup() # -- Project information ----------------------------------------------------- -project = 'Django Slick Reporting' -copyright = '2020, Ramez Ashraf' -author = 'Ramez Ashraf' +project = "Django Slick Reporting" +copyright = "2020, Ramez Ashraf" +author = "Ramez Ashraf" -master_doc = 'index' +master_doc = "index" # The full version, including alpha/beta/rc tags -release = '0.6.8' +release = "0.6.8" # -- General configuration --------------------------------------------------- @@ -37,12 +37,13 @@ autosummary_generate = True autoclass_content = "class" extensions = [ - 'sphinx.ext.viewcode', 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', + "sphinx.ext.viewcode", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -54,9 +55,9 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] diff --git a/runtests.py b/runtests.py index 5f753e1..72dd4ee 100644 --- a/runtests.py +++ b/runtests.py @@ -8,22 +8,24 @@ from django.test.utils import get_runner if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Run the Django Slick Reporting test suite.") + parser = argparse.ArgumentParser( + description="Run the Django Slick Reporting test suite." + ) parser.add_argument( - 'modules', nargs='*', metavar='module', + "modules", + nargs="*", + metavar="module", help='Optional path(s) to test modules; e.g. "i18n" or ' - '"i18n.tests.TranslationTests.test_lazy_objects".', + '"i18n.tests.TranslationTests.test_lazy_objects".', ) options = parser.parse_args() options.modules = [os.path.normpath(labels) for labels in options.modules] - - os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.test_settings' + os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings" django.setup() TestRunner = get_runner(settings) test_runner = TestRunner() failures = test_runner.run_tests(options.modules) # failures = test_runner.run_tests(["tests"]) sys.exit(bool(failures)) - diff --git a/tests/models.py b/tests/models.py index 2894675..2cae18e 100644 --- a/tests/models.py +++ b/tests/models.py @@ -55,12 +55,18 @@ class Contact(models.Model): class Client(models.Model): + class SexChoices(models.TextChoices): + FEMALE = "FEMALE", _("Female") + MALE = "MALE", _("Male") + OTHER = "OTHER", _("Other") + slug = models.CharField(max_length=200, verbose_name=_("Client Slug")) name = models.CharField(max_length=200, verbose_name=_("Name"), unique=True) email = models.EmailField(blank=True) notes = models.TextField() contact = models.ForeignKey(Contact, on_delete=models.CASCADE, null=True) + sex = models.CharField(max_length=10, choices=SexChoices.choices, default="OTHER") class Meta: verbose_name = _("Client") diff --git a/tests/test_settings.py b/tests/settings.py similarity index 100% rename from tests/test_settings.py rename to tests/settings.py diff --git a/tests/urls.py b/tests/urls.py index 4cacd98..f93f09b 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -20,7 +20,7 @@ ), path( "product_crosstab_client-to_field-set/", - views.ProductClientSalesMatrixToFIeldSet.as_view(), + views.ProductClientSalesMatrixToFieldSet.as_view(), name="product_crosstab_client_to_field_set", ), path( diff --git a/tests/views.py b/tests/views.py index 5c76a13..2e12cc0 100644 --- a/tests/views.py +++ b/tests/views.py @@ -43,7 +43,7 @@ class ProductClientSalesMatrix(SlickReportView): ] -class ProductClientSalesMatrixToFIeldSet(SlickReportView): +class ProductClientSalesMatrixToFieldSet(SlickReportView): report_title = "awesome report title" report_model = SimpleSales2 date_field = "doc_date" From b926f2c905dea74fd4eb86e168f91f5f4476629d Mon Sep 17 00:00:00 2001 From: Ramez Ashraf Date: Tue, 30 May 2023 16:08:25 +0300 Subject: [PATCH 05/28] - Add support to crosstab on traversing fields --- CHANGELOG.md | 2 ++ slick_reporting/generator.py | 10 ++++++++-- tests/report_generators.py | 16 +++++++++++++++- tests/test_generator.py | 11 +++++++++++ tests/tests.py | 6 +++--- 5 files changed, 39 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79a00b4..ea6a530 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ## [unreleased] - Allow cross tab on fields & deprecate `crosstab_model` in favor of crosstab_field, to be removed next version. - Add support for start_date_field_name and end_date_field_name +- Add support to crosstab on traversing fields +- Fix an issue if a foreign key have a custom `to_field` in group_by and `crosstab_field` ## [0.8.0] diff --git a/slick_reporting/generator.py b/slick_reporting/generator.py index a28d6f1..4171b1d 100644 --- a/slick_reporting/generator.py +++ b/slick_reporting/generator.py @@ -404,8 +404,14 @@ def _construct_crosstab_filter(self, col_data): :param col_data: :return: """ - field = get_field_from_query_text(col_data["crosstab_field"], self.report_model) - column_name = field.column + if "__" in col_data["crosstab_field"]: + column_name = col_data["crosstab_field"] + else: + field = get_field_from_query_text( + col_data["crosstab_field"], self.report_model + ) + column_name = field.column + # breakpoint() if col_data["is_remainder"]: filters = [~Q(**{f"{column_name}__in": self.crosstab_ids})] else: diff --git a/tests/report_generators.py b/tests/report_generators.py index 4dfca20..d900a4e 100644 --- a/tests/report_generators.py +++ b/tests/report_generators.py @@ -68,7 +68,21 @@ class CrosstabOnField(ReportGenerator): ] -# +class CrosstabOnTraversingField(ReportGenerator): + report_model = ComplexSales + date_field = "doc_date" + + group_by = "product" + columns = ["name"] + + crosstab_field = "client__sex" + crosstab_ids = ["FEMALE", "MALE", "OTHER"] + + crosstab_columns = [ + SlickReportField.create( + Sum, "quantity", name="value__sum", verbose_name=_("Sales") + ) + ] class ClientTotalBalance(ReportGenerator): diff --git a/tests/test_generator.py b/tests/test_generator.py index ac6b7b8..26b872a 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -16,6 +16,7 @@ GroupByCharField, TimeSeriesCustomDates, CrosstabOnField, + CrosstabOnTraversingField, ) from .tests import BaseTestData, year @@ -85,6 +86,16 @@ def test_crosstab_on_field(self): self.assertEqual(data[0]["value__sumCT----"], 77, data) self.assertEqual(data[1]["value__sumCTsales-return"], 34, data) + def test_crosstab_on_traversing_field(self): + report = CrosstabOnTraversingField() + data = report.get_report_data() + breakpoint() + self.assertEqual(len(data), 2, data) + self.assertEqual(data[0]["value__sumCTOTHER"], 120, data) + self.assertEqual(data[0]["value__sumCTFEMALE"], 77, data) + self.assertEqual(data[0]["value__sumCT----"], 0, data) + self.assertEqual(data[1]["value__sumCTOTHER"], 34, data) + class GeneratorReportStructureTest(BaseTestData, TestCase): @classmethod diff --git a/tests/tests.py b/tests/tests.py index db7ece0..891ebf4 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -56,13 +56,13 @@ def setUpTestData(cls): cls.limited_user = limited_user agent = Agent.objects.create(name="John") agent2 = Agent.objects.create(name="Frank") - cls.client1 = Client.objects.create(name="Client 1") + cls.client1 = Client.objects.create(name="Client 1", sex="MALE") cls.client1.contact = Contact.objects.create(address="Street 1", agent=agent) cls.client1.save() - cls.client2 = Client.objects.create(name="Client 2") + cls.client2 = Client.objects.create(name="Client 2", sex="FEMALE") cls.client2.contact = Contact.objects.create(address="Street 2", agent=agent) cls.client2.save() - cls.client3 = Client.objects.create(name="Client 3") + cls.client3 = Client.objects.create(name="Client 3", sex="OTHER") cls.client3.contact = Contact.objects.create(address="Street 3", agent=agent2) cls.client3.save() cls.clientIdle = Client.objects.create(name="Client Idle") From 49b2cc225f68e19a947bf3b7127db795b1c30d29 Mon Sep 17 00:00:00 2001 From: Ramez Ashraf Date: Wed, 31 May 2023 16:08:34 +0300 Subject: [PATCH 06/28] Easy override to the search form, By creating you own form and subclass BaseReportForm and implement the mandatory method(s) --- CHANGELOG.md | 3 +- slick_reporting/form_factory.py | 56 ++++++++++++++++++++++++++++++++- slick_reporting/views.py | 34 ++++++++++++++------ 3 files changed, 82 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea6a530..f61880a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ All notable changes to this project will be documented in this file. - Allow cross tab on fields & deprecate `crosstab_model` in favor of crosstab_field, to be removed next version. - Add support for start_date_field_name and end_date_field_name - Add support to crosstab on traversing fields -- Fix an issue if a foreign key have a custom `to_field` in group_by and `crosstab_field` +- Fix an issue if a foreign key have a custom `to_field` in group_by and `crosstab_field` +- Easy override to the search form, By creating you own form and subclass BaseReportForm and implement the mandatory method(s) ## [0.8.0] diff --git a/slick_reporting/form_factory.py b/slick_reporting/form_factory.py index c661512..8e21c26 100644 --- a/slick_reporting/form_factory.py +++ b/slick_reporting/form_factory.py @@ -31,6 +31,7 @@ def get_crispy_helper( from crispy_forms.helper import FormHelper from crispy_forms.layout import Column, Layout, Div, Row, Field + foreign_keys_map = foreign_keys_map or [] helper = FormHelper() helper.form_class = "form-horizontal" helper.label_class = "col-sm-2 col-md-2 col-lg-2" @@ -64,6 +65,50 @@ def get_crispy_helper( class BaseReportForm: + def get_filters(self): + raise NotImplemented( + "get_filters() must be implemented in subclass," + "should return a tuple of (Q objects, kwargs filter) to be passed to QuerySet.filter()" + ) + + def get_start_date(self): + raise NotImplemented( + "get_start_date() must be implemented in subclass," + "should return a datetime object" + ) + + def get_end_date(self): + raise NotImplemented( + "get_end_date() must be implemented in subclass," + "should return a datetime object" + ) + + def get_crosstab_compute_remainder(self): + raise NotImplemented( + "get_crosstab_compute_remainder() must be implemented in subclass," + "should return a boolean value" + ) + + def get_crosstab_ids(self): + raise NotImplemented( + "get_crosstab_ids() must be implemented in subclass," + "should return a list of ids to be used for crosstab" + ) + + def get_time_series_pattern(self): + raise NotImplemented( + "get_time_series_pattern() must be implemented in subclass," + "should return a string value of a valid time series pattern" + ) + + def get_crispy_helper(self): + raise NotImplemented( + "get_crispy_helper() must be implemented in subclass," + "should return a crispy helper object" + ) + + +class SlickReportForm(BaseReportForm): """ Holds basic function """ @@ -77,6 +122,15 @@ def media(self): js=SLICK_REPORTING_FORM_MEDIA.get("js", []), ) + def get_start_date(self): + return self.cleaned_data.get("start_date") + + def get_end_date(self): + return self.cleaned_data.get("end_date") + + def get_time_series_pattern(self): + return self.cleaned_data.get("time_series_pattern") + def get_filters(self): """ Get the foreign key filters for report queryset, excluding crosstab ids, handled by `get_crosstab_ids()` @@ -232,7 +286,7 @@ def report_form_factory( crosstab_field_related_name = crosstab_field_klass[0].to_fields[0] bases = ( - BaseReportForm, + SlickReportForm, forms.BaseForm, ) new_form = type( diff --git a/slick_reporting/views.py b/slick_reporting/views.py index c23f6f5..dc299ee 100644 --- a/slick_reporting/views.py +++ b/slick_reporting/views.py @@ -101,6 +101,8 @@ class SlickReportViewBase(FormView): chart_settings = None crosstab_model = None + crosstab_field = None + crosstab_ids = None crosstab_columns = None crosstab_compute_remainder = True @@ -231,12 +233,12 @@ def get_report_generator(self, queryset, for_print): time_series_pattern = self.time_series_pattern if self.time_series_selector: - time_series_pattern = self.form.cleaned_data["time_series_pattern"] + time_series_pattern = self.form.get_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"], + start_date=self.form.get_start_date(), + end_date=self.form.get_end_date(), q_filters=q_filters, kwargs_filters=kw_filters, date_field=self.date_field, @@ -332,7 +334,13 @@ def get_form_initial(): } def get_form_crispy_helper(self): - return self.form.get_crispy_helper() + """ + A hook retuning crispy helper for the form + :return: + """ + if hasattr(self, "form"): + return self.form.get_crispy_helper() + return None def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -371,6 +379,9 @@ class SlickReportingListView(SlickReportViewBase): filters = None def get_form_filters(self, form): + if self.form_class: + return form.get_filters() + kw_filters = {} for name, field in form.base_fields.items(): @@ -413,11 +424,16 @@ def get_report_generator(self, queryset, for_print): ) def get_form_class(self): - return modelform_factory( - self.get_report_model(), - fields=self.filters, - formfield_callback=default_formfield_callback, - ) + if self.form_class: + return self.form_class + + elif self.filters: + return modelform_factory( + self.get_report_model(), + fields=self.filters, + formfield_callback=default_formfield_callback, + ) + return forms.Form def get_report_results(self, for_print=False): """ From 66b9090d407c2b5c1889e91bc048fbca47d947c6 Mon Sep 17 00:00:00 2001 From: Ramez Ashraf Date: Thu, 1 Jun 2023 16:30:02 +0300 Subject: [PATCH 07/28] * Add documentation to override the form * encapsulate the resources needed in a file to be included --- docs/source/the_view.rst | 105 +++++++++++++++++- slick_reporting/{form_factory.py => forms.py} | 0 .../templates/slick_reporting/base.html | 29 ++--- .../slick_reporting/js_resources.html | 42 +++++++ .../slick_reporting/simple_report.html | 77 +------------ slick_reporting/views.py | 2 +- 6 files changed, 156 insertions(+), 99 deletions(-) rename slick_reporting/{form_factory.py => forms.py} (100%) create mode 100644 slick_reporting/templates/slick_reporting/js_resources.html diff --git a/docs/source/the_view.rst b/docs/source/the_view.rst index 76e8695..1a24c84 100644 --- a/docs/source/the_view.rst +++ b/docs/source/the_view.rst @@ -44,14 +44,107 @@ filters: a list of report_model fields to be used as filters. Override the Form ------------------ -The Form purposes are +The default form is generated by ``slick_reporting.form_factory.report_form_factory``. -1. Provide the start_date and end_date -2. provide a ``get_filters`` method which return a tuple (Q_filers , kwargs filter) to be used in filtering. - q_filter: can be none or a series of Django's Q queries - kwargs_filter: None or a dict of filters +The system expect the form to implement the ``slick_reporting.forms.BaseReportForm`` interface. -Following those 2 recommendation, your awesome custom form will work as you'd expect. +The methods to implement are: + +* ``get_filters``: return a tuple (Q_filers , kwargs filter) to be used in filtering. + q_filter: can be none or a series of Django's Q queries + kwargs_filter: None or a dictionary of filters + +* ``get_start_date``: return the start date of the report. + +* ``get_end_date``: return the end date of the report. + +* ``get_crispy_helper`` : return a crispy form helper to be used in rendering the form. (optional) + +In case you are working with a crosstab report, you need to implement the following methods: + +* ``get_crosstab_compute_remainder``: return a boolean indicating if the remainder should be computed or not. + +* ``get_crosstab_ids``: return a list of ids to be used in the crosstab report. + + +And in case you are working with a time series report, with a selector on, you need to implement the following method: + +* ``get_time_series_pattern``: return a string representing the time series pattern. ie: ``ie: daily, monthly, yearly`` + +Example: + +.. code-block:: python + + # forms.py + from slick_reporting.forms import BaseReportForm + + # Inherit from BaseReportForm + class RequestLogForm(BaseReportForm, forms.Form): + + SECURE_CHOICES = ( + ("all", "All"), + ("secure", "Secure"), + ("non-secure", "Not Secure"), + ) + + start_date = forms.DateField( + required=False, + label="Start Date", + widget=forms.DateInput(attrs={"type": "date"}), + ) + end_date = forms.DateField( + required=False, label="End Date", widget=forms.DateInput(attrs={"type": "date"}) + ) + secure = forms.ChoiceField( + choices=SECURE_CHOICES, required=False, label="Secure", initial="all" + ) + method = forms.CharField(required=False, label="Method") + response = forms.ChoiceField( + choices=HTTP_STATUS_CODES, + required=False, + label="Response", + initial="200", + ) + other_people_only = forms.BooleanField( + required=False, label="Show requests from other People Only" + ) + + def __init__(self, request=None, *args, **kwargs): + self.request = request + super(RequestLogForm, self).__init__(*args, **kwargs) + # provide initial values and ay needed customization + self.fields["start_date"].initial = datetime.date.today() + self.fields["end_date"].initial = datetime.date.today() + + def get_filters(self): + # return the filters to be used in the report + # Note the use of Q filters and kwargs filters + filters = {} + q_filters = [] + if self.cleaned_data["secure"] == "secure": + filters["is_secure"] = True + elif self.cleaned_data["secure"] == "non-secure": + filters["is_secure"] = False + if self.cleaned_data["method"]: + filters["method"] = self.cleaned_data["method"] + if self.cleaned_data["response"]: + filters["response"] = self.cleaned_data["response"] + if self.cleaned_data["other_people_only"]: + q_filters.append(~Q(user=self.request.user)) + + return q_filters, filters + + def get_start_date(self): + return self.cleaned_data["start_date"] + + def get_end_date(self): + return self.cleaned_data["end_date"] + + # reports.py + + @register_report_view + class RequestCountByPath(ReportView): + form_class = RequestLogForm Charting diff --git a/slick_reporting/form_factory.py b/slick_reporting/forms.py similarity index 100% rename from slick_reporting/form_factory.py rename to slick_reporting/forms.py diff --git a/slick_reporting/templates/slick_reporting/base.html b/slick_reporting/templates/slick_reporting/base.html index 950ba91..299d07d 100644 --- a/slick_reporting/templates/slick_reporting/base.html +++ b/slick_reporting/templates/slick_reporting/base.html @@ -25,28 +25,19 @@ - - +{##} +{##} {#Date picker #} - - - - -{#select2#} - - - - -{# datatable #} - - - +{##} +{##} +{##} +{% include "slick_reporting/js_resources.html" %} {% block extrajs %} {% endblock %} diff --git a/slick_reporting/templates/slick_reporting/js_resources.html b/slick_reporting/templates/slick_reporting/js_resources.html new file mode 100644 index 0000000..9f97213 --- /dev/null +++ b/slick_reporting/templates/slick_reporting/js_resources.html @@ -0,0 +1,42 @@ +{% load i18n static %} + + + + + +{##} +{##} + + + + + + + + + + + + + + + + + + + + + + + diff --git a/slick_reporting/templates/slick_reporting/simple_report.html b/slick_reporting/templates/slick_reporting/simple_report.html index 6e2efd3..8cd890b 100644 --- a/slick_reporting/templates/slick_reporting/simple_report.html +++ b/slick_reporting/templates/slick_reporting/simple_report.html @@ -59,9 +59,9 @@

Results

{% endblock %} {% block extrajs %} {{ block.super }} - - - +{# #} +{# #} +{# #} #} {##} + + + - diff --git a/slick_reporting/views.py b/slick_reporting/views.py index 95231b1..4decbda 100644 --- a/slick_reporting/views.py +++ b/slick_reporting/views.py @@ -25,6 +25,7 @@ ReportGenerator, ListViewReportGenerator, ReportGeneratorAPI, + Chart, # needed for easier importing in other apps ) From e836d78d61a6a9a17f573c5aa5fedd76d57ebaab Mon Sep 17 00:00:00 2001 From: Ramez Ashraf Date: Sun, 4 Jun 2023 17:34:46 +0300 Subject: [PATCH 17/28] Adds form in valid response Further Cleans/consolidate statics --- .../templates/slick_reporting/simple_report.html | 11 ----------- slick_reporting/views.py | 11 +++++++++-- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/slick_reporting/templates/slick_reporting/simple_report.html b/slick_reporting/templates/slick_reporting/simple_report.html index 8cd890b..a29ad02 100644 --- a/slick_reporting/templates/slick_reporting/simple_report.html +++ b/slick_reporting/templates/slick_reporting/simple_report.html @@ -59,22 +59,11 @@

Results

{% endblock %} {% block extrajs %} {{ block.super }} -{# #} -{# #} -{# #} - - - -