From e11f1fffcc51c70974fbf0cc9609f8142f77b3b6 Mon Sep 17 00:00:00 2001 From: Ramez Ashraf Date: Tue, 10 Oct 2023 23:21:08 +0300 Subject: [PATCH] Enhance Report View documentation --- docs/source/ref/index.rst | 3 +- docs/source/ref/view_options.rst | 303 +++++++++++++++++-------- docs/source/topics/group_by_report.rst | 3 + slick_reporting/views.py | 10 +- tests/report_generators.py | 11 +- 5 files changed, 224 insertions(+), 106 deletions(-) diff --git a/docs/source/ref/index.rst b/docs/source/ref/index.rst index f81e27b..eba6f5c 100644 --- a/docs/source/ref/index.rst +++ b/docs/source/ref/index.rst @@ -10,8 +10,9 @@ Below are links to the reference documentation for the various components of the :caption: Components: settings + view_options computation_field report_generator - view_options + diff --git a/docs/source/ref/view_options.rst b/docs/source/ref/view_options.rst index be5940e..1701299 100644 --- a/docs/source/ref/view_options.rst +++ b/docs/source/ref/view_options.rst @@ -1,152 +1,221 @@ .. _report_view_options: -General Options +================ +The Report View ================ +Below is the list of options that can be used in the ReportView class. -Below is the list of general options that is used across all types of reports. +Core Options +============= -.. attribute:: ReportView.report_model +report_model +------------ - The model where the relevant data is stored, in more complex reports, it's usually a database view / materialized view. +The model where the relevant data is stored, in more complex reports, +it's usually a database view / materialized view. +You can customize it at runtime via the ``get_report_model`` hook. -.. attribute:: ReportView.queryset +.. code-block:: python - The queryset to be used in the report, if not specified, it will default to ``report_model._default_manager.all()`` + class MyReportView(ReportView): + def get_report_model(self): + from my_app.models import MyReportModel + return MyReportModel.objects.filter(some_field__isnull=False) -.. attribute:: ReportView.columns - Columns can be a list of column names , or a tuple of (column name, options dictionary) pairs. +queryset +-------- - Example: +The queryset to be used in the report, +if not specified, it will default to ``report_model._default_manager.all()`` - .. code-block:: python +group_by +-------- - class MyReport(ReportView): - columns = [ - "id", - ("name", {"verbose_name": "My verbose name", "is_summable": False}), - "description", - # A callable on the view /or the generator, that takes the record as a parameter and returns a value. - ("get_full_name", {"verbose_name": "Full Name", "is_summable": False}), - ] +If the data in the report_model needs to be grouped by a field. +It can be a foreign key, a text field / choice field on the report model or traversing. - def get_full_name(self, record): - return record["first_name"] + " " + record["last_name"] +Example: +Assuming we have the following SalesModel +.. code-block:: python + + class SalesModel(models.Model): + date = models.DateTimeField() + notes = models.TextField(blank=True, null=True) + client = models.ForeignKey( + "client.Client", on_delete=models.PROTECT, verbose_name=_("Client") + ) + product = models.ForeignKey( + "product.Product", on_delete=models.PROTECT, verbose_name=_("Product") + ) + value = models.DecimalField(max_digits=9, decimal_places=2) + quantity = models.DecimalField(max_digits=9, decimal_places=2) + price = models.DecimalField(max_digits=9, decimal_places=2) + +Our ReportView can have the following group_by options: + +.. code-block:: python + + from slick_reporting.views import ReportView + + class MyReport(ReportView): + report_model = SalesModel + group_by = "product" # a field on the model + # OR + # group_by = 'client__country' a traversing foreign key field + # group_by = 'client__gender' a traversing choice field - Here is a list of all available column options available. A column can be - * A Computation Field. Added as a class or by its name if its registered see :ref:`computation_field` - Example: - .. code-block:: python +columns +------- +Columns are a list of column names and to make it more flexible, +you can pass a tuple of column name and options. +The options are only `verbose_name` and `is_summable`. - class MyTotalReportField(ComputationField): - pass +like this: +.. code-block:: python - class MyReport(ReportView): - columns = [ - # a computation field created on the fly - ComputationField.create(Sum, "value", verbose_name=_("Value"), name="value"), - # A computation Field class - MyTotalReportField, - # a computation field registered in the computation field registry - "__total__", + class MyReport(ReportView): + columns = [ + "id", + ("name", {"verbose_name": "My verbose name", "is_summable": False}), ] +A column name can be any of the following: - * If group_by is set and it's a foreign key, then any field on the grouped by model. +1. A computation field +2. A field on the grouped by model +3. A callable on the view /or the generator +4. A Special ``__time_series__``, ``__crosstab__``, ``__index__`` - Example: +Let's take them one by one: - .. code-block:: python +1. A Computation Field. +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Added as a class or by its name. +Example: + +.. code-block:: python + + from slick_reporting.fields import ComputationField, Sum + from slick_reporting.registry import field_registry + from slick_reporting.views import ReportView + + @field_registry.register + class MyTotalReportField(ComputationField): + name = "__some_special_name__" class MyReport(ReportView): - report_model = MySales - group_by = "client" columns = [ - "name", # field that exists on the Client Model - "date_of_birth", # field that exists on the Client Model - "agent__name", # field that exists on the Agent Model related to the Client Model - # calculation fields + ComputationField.create(Sum, "value", verbose_name=_("Value"), name="value"), + # a computation field created on the fly + + MyTotalReportField, # Added a a class + + "__some_special_name__", # added by name ] +For more information: :ref:`computation_field` +2. Fields on the group by model +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - * If group_by is not set, then - 1. Any field name on the report_model / queryset - 2. A calculation field, in this case the calculation will be made on the whole set of records, not on each group. - Example: +Implying that the group_by is set to a field on the report_model. - .. code-block:: python +.. code-block:: python - class MyReport(ReportView): - report_model = MySales - group_by = None - columns = [ - ComputationField.create(Sum, "value", verbose_name=_("Value"), name="value") - ] + class MyReport(ReportView): + report_model = SalesModel + group_by = "client" + columns = [ + "name", # field that exists on the Client Model + "date_of_birth", # field that exists on the Client Model + "agent__name", # a traversing field from client model + # ... + ] - Above code will return the calculated sum of all values in the report_model / queryset + # If the group_by is traversing then the available columns would be of the model at the end of the traversing + class MyOtherReport(ReportView): + report_model = MySales + group_by = "client__agent" + columns = [ + "name", + "country", # fields that exists on the Agent Model + "contact__email", # A traversing field from the Agent model + ] - * A callable on the view /or the generator, that takes the record as a parameter and returns a value. - * A Special ``__time_series__``, and ``__crosstab__`` +.. note:: - Those are used to control the position of the time series inside the columns, defaults it's appended at the end + If group_by is not set, columns can be only a calculation field. refer to the topic `no_group_by_topic` -.. attribute:: ReportView.date_field +3. A callable on the view +~~~~~~~~~~~~~~~~~~~~~~~~~~~ - the date field to be used in filtering and computing +The callable should accept the following arguments -.. attribute:: ReportView.start_date_field_name + :param obj: a dictionary of the current group_by row + :param row: a the current row of the report. + :return: the value to be displayed in the report - the name of the start date field, if not specified, it will default to ``date_field`` -.. attribute:: ReportView.end_date_field_name +.. code-block:: python - the name of the end date field, if not specified, it will default to ``date_field`` + class Report(ReportView): + columns = [ "field_on_group_by_model", "group_by_model__traversing_field", + "get_attribute", ComputationField.create(name="example")] + def get_attribute(self, obj: dict, row: dict): + # obj: a dictionary of the current group_by row + # row: a the current row of the report. -.. attribute:: ReportView.group_by + return f"{obj["field_on_group_by_model_2"]} - {row["group_by_model__traversing_field"]}" - the group by field, it can be a foreign key, a text field, on the report model or traversing a foreign key. + get_attribute.verbose_name = "My awesome title" - Example: - .. code-block:: python +4. A Special ``__time_series__``, ``__crosstab__``, ``__index__`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``__time_series__``: is used to control the position of the time series columns inside the report. + +``__crosstab__``: is used to control the position of the crosstab columns inside the report. + +``__index__``: is used to display the index of the report, it's usually used with the ``group_by_custom_querysets`` option. - class MyReport(ReportView): - report_model = MySalesModel - group_by = "client" - # OR - # group_by = 'client__agent__name' - # OR - # group_by = 'client__agent' -.. attribute:: ReportView.report_title +date_field +---------- - the title of the report to be displayed in the report page. +The date field to be used in filtering and computing -.. attribute:: ReportView.report_title_context_key +start_date_field_name +--------------------- +The name of the start date field, if not specified, it will default to what set in ``date_field`` - the context key to be used to pass the report title to the template, default to ``title``. +end_date_field_name +------------------- +The name of the end date field, if not specified, it will default to ``date_field`` -.. attribute:: ReportView.chart_settings - A list of Chart objects representing the charts you want to attach to the report. +chart_settings +-------------- +A list of Chart objects representing the charts you want to attach to the report. Example: @@ -157,11 +226,11 @@ Below is the list of general options that is used across all types of reports. # .. chart_settings = [ Chart( - "Browsers", - Chart.PIE, + title="Browsers", + type=Chart.PIE, # or just string "bar" title_source=["user_agent"], data_source=["count__id"], - plot_total=True, + plot_total=False, ), Chart( "Browsers Bar Chart", @@ -173,32 +242,70 @@ Below is the list of general options that is used across all types of reports. ] -.. attribute:: ReportView.default_order_by +form_class +---------- +The form you need to display to control the results. +Default to an automatically generated form containing the start date, end date and all foreign keys on the model. +For more information: `filter_form` + +excluded_fields +----------------- +Fields to be excluded from the automatically generated form + + +auto_load +-------------- +Control if the report should be loaded automatically on page load or not, default to ``True`` + + +``report_title`` +---------------- +The title of the report to be displayed in the report page. + +``report_title_context_key`` +---------------------------- +The context key to be used to pass the report title to the template, default to ``report_title``. + + + +``template_name`` +----------------- + +The template to be used to render the report, default to ``slick_reporting/report.html`` +You can override this to customize the report look and feel. + - Default order by for the results. Ordering can also be controlled on run time by passing order_by='field_name' as a parameter to the view. - As you would expect, for DESC order: default_order_by (or order_by as a parameter) ='-field_name' +``csv_export_class`` +-------------------- +Set the csv export class to be used to export the report, default to ``ExportToStreamingCSV`` -.. attribute:: ReportView.template_name - The template to be used to render the report, default to ``slick_reporting/simple_report.html`` - You can override this to customize the report look and feel. +``report_generator_class`` +-------------------------- +Set the generator class to be used to generate the report, default to ``ReportGenerator`` -.. attribute:: ReportView.limit_records +``default_order_by`` +-------------------- +A Default order by for the results. +As you would expect, for DESC order: default_order_by (or order_by as a parameter) ='-field_name' - Limit the number of records to be displayed in the report, default to ``None`` (no limit) +.. note:: -.. attribute:: ReportView.swap_sign + Ordering can also be controlled at run time by passing order_by='field_name' as a parameter to the view. - Swap the sign of the values in the report, default to ``False`` +``limit_records`` +----------------- -.. attribute:: ReportView.csv_export_class +Limit the number of records to be displayed in the report, default to ``None`` (no limit) - Set the csv export class to be used to export the report, default to ``ExportToStreamingCSV`` +``swap_sign`` +-------------- +Swap the sign of the values in the report, default to ``False`` -.. attribute:: ReportView.report_generator_class - Set the generator class to be used to generate the report, default to ``ReportGenerator`` +Double Sided Calculations Options +================================== .. attribute:: ReportView.with_type diff --git a/docs/source/topics/group_by_report.rst b/docs/source/topics/group_by_report.rst index 82735ed..c40708d 100644 --- a/docs/source/topics/group_by_report.rst +++ b/docs/source/topics/group_by_report.rst @@ -69,6 +69,7 @@ Example: group_by = "product__product_category" # Note the traversing +.. _group_by_custom_querysets_topic: Group by custom querysets ------------------------- @@ -133,6 +134,8 @@ its verbose name (ie the one on the table header) can be customized via ``group_ You can then customize the *value* of the __index__ column via ``format_row`` hook +.. _no_group_by_topic: + The No Group By --------------- Sometimes you want to get some calculations done on the whole report_model, without a group_by. diff --git a/slick_reporting/views.py b/slick_reporting/views.py index d242003..a1042bd 100644 --- a/slick_reporting/views.py +++ b/slick_reporting/views.py @@ -118,10 +118,10 @@ class ReportViewBase(ReportGeneratorAPI, FormView): export_actions = None - @staticmethod - def form_filter_func(fkeys_dict): - # todo revise - return fkeys_dict + # @staticmethod + # def form_filter_func(fkeys_dict): + # # todo revise + # return fkeys_dict @classmethod def get_report_title(cls): @@ -250,7 +250,7 @@ def get_form_class(self): crosstab_model=self.crosstab_field, display_compute_remainder=self.crosstab_compute_remainder, excluded_fields=self.excluded_fields, - fkeys_filter_func=self.form_filter_func, + fkeys_filter_func=None, initial=self.get_initial(), show_time_series_selector=self.time_series_selector, time_series_selector_choices=self.time_series_selector_choices, diff --git a/tests/report_generators.py b/tests/report_generators.py index c713a3a..914c7b4 100644 --- a/tests/report_generators.py +++ b/tests/report_generators.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime from django.db.models import Sum, Count @@ -34,7 +36,7 @@ class GeneratorWithAttrAsColumn(GenericGenerator): columns = ["get_data", "slug", "name"] def get_data(self, obj): - return "" + return obj["name"] get_data.verbose_name = "My Verbose Name" @@ -166,7 +168,12 @@ class ProductTotalSales(ReportGenerator): "average_value", ] - def get_object_sku(self, obj, data): + def get_object_sku(self, obj: dict, row: dict) -> any: + """ + :param obj: obj is the current row of the grouped by model , or the current row of the queryset + :param row: the current report row values in a dictionary + :return: + """ return obj["sku"].upper() get_object_sku.verbose_name = "SKU ALL CAPS"