diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml new file mode 100644 index 0000000..47013f2 --- /dev/null +++ b/.github/workflows/django.yml @@ -0,0 +1,30 @@ +name: Django CI + +on: + push: + branches: [ "develop" ] + pull_request: + branches: [ "develop" ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: [3.8, 3.9, "3.10", 3.11] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r tests/requirements.txt + - name: Run Tests + run: | + python runtests.py diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 9f07820..823a3f3 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,9 +1,9 @@ version: 2 build: - os: "ubuntu-20.04" + os: "ubuntu-22.04" tools: - python: "3.8" + python: "3.11" # Build from the docs/ directory with Sphinx sphinx: diff --git a/CHANGELOG.md b/CHANGELOG.md index 44c9e2b..177c64a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to this project will be documented in this file. +## [1.1.1] - 2023-09-25 +- Change settings to be a dict , adding support JQUERY_URL and FONT AWESOME customization #79 & #81 +- Fix issue with chartjs not being loaded #80 +- Remove `SLICK_REPORTING_FORM_MEDIA` + ## [1.1.0] - - Breaking: changed ``report_title_context_key`` default value to `report_title` - Breaking: Renamed simple_report.html to report.html diff --git a/README.rst b/README.rst index 18e8c09..cb97be3 100644 --- a/README.rst +++ b/README.rst @@ -64,8 +64,12 @@ Let's start by a "Group by" report. This will generate a report how much quantit group_by = "product" columns = [ "name", - ComputationField.create(Sum, "quantity", verbose_name="Total quantity sold", is_summable=False), - ComputationField.create(Sum, "value", name="sum__value", verbose_name="Total Value sold $"), + ComputationField.create( + Sum, "quantity", verbose_name="Total quantity sold", is_summable=False + ), + ComputationField.create( + Sum, "value", name="sum__value", verbose_name="Total Value sold $" + ), ] chart_settings = [ @@ -83,6 +87,7 @@ Let's start by a "Group by" report. This will generate a report how much quantit ), ] + # then, in urls.py path("total-sales-report", TotalProductSales.as_view()) @@ -122,7 +127,7 @@ Example: How much was sold in value for each product monthly within a date perio time_series_columns = [ ComputationField.create( Sum, "value", verbose_name=_("Sales Value"), name="value" - ) # what will be calculated for each month + ) # what will be calculated for each month ] chart_settings = [ @@ -133,12 +138,13 @@ Example: How much was sold in value for each product monthly within a date perio title_source=["name"], plot_total=True, ), - Chart("Total Sales [Area chart]", - Chart.AREA, - data_source=["value"], - title_source=["name"], - plot_total=False, - ) + Chart( + "Total Sales [Area chart]", + Chart.AREA, + data_source=["value"], + title_source=["name"], + plot_total=False, + ), ] @@ -233,28 +239,20 @@ You can also use locally the ``create_entries`` command will generate data for the demo app +Documentation +------------- -Batteries Included ------------------- - -Slick Reporting comes with - -* An auto-generated, bootstrap-ready Filter Form -* Carts.js Charting support `Chart.js `_ -* Highcharts.js Charting support `Highcharts.js `_ -* Datatables `datatables.net `_ +Available on `Read The Docs `_ -A Preview: +You can run documentation locally -.. image:: https://i.ibb.co/SvxTM23/Selection-294.png - :target: https://i.ibb.co/SvxTM23/Selection-294.png - :alt: Shipped in View Page +.. code-block:: console + + cd docs + pip install -r requirements.txt + sphinx-build -b html source build -Documentation -------------- - -Available on `Read The Docs `_ Road Ahead ---------- diff --git a/demo_proj/demo_app/helpers.py b/demo_proj/demo_app/helpers.py index a1be416..a639d04 100644 --- a/demo_proj/demo_app/helpers.py +++ b/demo_proj/demo_app/helpers.py @@ -11,7 +11,6 @@ ("product-sales-per-country-crosstab", reports.ProductSalesPerCountryCrosstab), ("last-10-sales", reports.LastTenSales), ("total-product-sales-with-custom-form", reports.TotalProductSalesWithCustomForm), - ] GROUP_BY = [ @@ -27,7 +26,7 @@ ("time-series-with-custom-dates", reports.TimeSeriesReportWithCustomDates), ("time-series-with-custom-dates-and-title", reports.TimeSeriesReportWithCustomDatesAndCustomTitle), ("time-series-without-group-by", reports.TimeSeriesWithoutGroupBy), - ('time-series-with-group-by-custom-queryset', reports.TimeSeriesReportWithCustomGroupByQueryset), + ("time-series-with-group-by-custom-queryset", reports.TimeSeriesReportWithCustomGroupByQueryset), ] CROSSTAB = [ @@ -38,14 +37,14 @@ ("crosstab-report-custom-verbose-name", reports.CrossTabReportWithCustomVerboseName), ("crosstab-report-custom-verbose-name-2", reports.CrossTabReportWithCustomVerboseNameCustomFilter), ("crosstab-report-with-time-series", reports.CrossTabWithTimeSeries), - +] +OTHER = [ + ("chartjs-examples", reports.ChartJSExample), ] def get_urls_patterns(): urls = [] - for name, report in TUTORIAL + GROUP_BY + TIME_SERIES + CROSSTAB: + for name, report in TUTORIAL + GROUP_BY + TIME_SERIES + CROSSTAB + OTHER: urls.append(path(f"{name}/", report.as_view(), name=name)) return urls - - diff --git a/demo_proj/demo_app/reports.py b/demo_proj/demo_app/reports.py index fe2d24f..30cf19d 100644 --- a/demo_proj/demo_app/reports.py +++ b/demo_proj/demo_app/reports.py @@ -19,7 +19,11 @@ class ProductSales(ReportView): columns = [ "name", ComputationField.create( - method=Sum, field="value", name="value__sum", verbose_name="Total sold $", is_summable=True, + method=Sum, + field="value", + name="value__sum", + verbose_name="Total sold $", + is_summable=True, ), ] @@ -198,7 +202,11 @@ class GroupByReport(ReportView): columns = [ "name", ComputationField.create( - method=Sum, field="value", name="value__sum", verbose_name="Total sold $", is_summable=True, + method=Sum, + field="value", + name="value__sum", + verbose_name="Total sold $", + is_summable=True, ), ] @@ -252,13 +260,13 @@ class GroupByCustomQueryset(ReportView): def format_row(self, row_obj): # Put the verbose names we need instead of the integer index - index = row_obj['__index__'] + index = row_obj["__index__"] if index == 0: row_obj["__index__"] = "Big" elif index == 1: - row_obj['__index__'] = "Small" + row_obj["__index__"] = "Small" elif index == 2: - row_obj['__index__'] = "Medium" + row_obj["__index__"] = "Medium" return row_obj @@ -270,7 +278,11 @@ class NoGroupByReport(ReportView): columns = [ ComputationField.create( - method=Sum, field="value", name="value__sum", verbose_name="Total sold $", is_summable=True, + method=Sum, + field="value", + name="value__sum", + verbose_name="Total sold $", + is_summable=True, ), ] @@ -292,29 +304,30 @@ class TimeSeriesReport(ReportView): "name", "__time_series__", # placeholder for the generated time series columns - ComputationField.create(Sum, "value", verbose_name=_("Total Sales")), # This is the same as the time_series_columns, but this one will be on the whole set - ] chart_settings = [ - Chart("Client Sales", - Chart.BAR, - data_source=["sum__value"], - title_source=["name"], - ), - Chart("Total Sales [Pie]", - Chart.PIE, - data_source=["sum__value"], - title_source=["name"], - plot_total=True, - ), - Chart("Total Sales [Area chart]", - Chart.AREA, - data_source=["sum__value"], - title_source=["name"], - ) + Chart( + "Client Sales", + Chart.BAR, + data_source=["sum__value"], + title_source=["name"], + ), + Chart( + "Total Sales [Pie]", + Chart.PIE, + data_source=["sum__value"], + title_source=["name"], + plot_total=True, + ), + Chart( + "Total Sales [Area chart]", + Chart.AREA, + data_source=["sum__value"], + title_source=["name"], + ), ] @@ -354,8 +367,8 @@ class TimeSeriesReportWithCustomGroupByQueryset(ReportView): report_title = _("Time Series Report") report_model = SalesTransaction group_by_custom_querysets = ( - SalesTransaction.objects.filter(client__country='US'), - SalesTransaction.objects.filter(client__country__in=['RS', 'DE']), + SalesTransaction.objects.filter(client__country="US"), + SalesTransaction.objects.filter(client__country__in=["RS", "DE"]), ) time_series_pattern = "monthly" @@ -370,29 +383,30 @@ class TimeSeriesReportWithCustomGroupByQueryset(ReportView): "__index__", "__time_series__", # placeholder for the generated time series columns - ComputationField.create(Sum, "value", verbose_name=_("Total Sales")), # This is the same as the time_series_columns, but this one will be on the whole set - ] chart_settings = [ - Chart("Client Sales", - Chart.BAR, - data_source=["sum__value"], - title_source=["__index__"], - ), - Chart("Total Sales [Pie]", - Chart.PIE, - data_source=["sum__value"], - title_source=["__index__"], - plot_total=True, - ), - Chart("Total Sales [Area chart]", - Chart.AREA, - data_source=["sum__value"], - title_source=["name"], - ) + Chart( + "Client Sales", + Chart.BAR, + data_source=["sum__value"], + title_source=["__index__"], + ), + Chart( + "Total Sales [Pie]", + Chart.PIE, + data_source=["sum__value"], + title_source=["__index__"], + plot_total=True, + ), + Chart( + "Total Sales [Area chart]", + Chart.AREA, + data_source=["sum__value"], + title_source=["name"], + ), ] @@ -421,17 +435,19 @@ class TimeSeriesReportWithCustomDatesAndCustomTitle(TimeSeriesReportWithCustomDa ] chart_settings = [ - Chart("Client Sales", - Chart.BAR, - data_source=["sum_of_value"], # Note: This is the name of our `TotalSalesField` `field - title_source=["name"], - ), - Chart("Total Sales [Pie]", - Chart.PIE, - data_source=["sum_of_value"], - title_source=["name"], - plot_total=True, - ), + Chart( + "Client Sales", + Chart.BAR, + data_source=["sum_of_value"], # Note: This is the name of our `TotalSalesField` `field + title_source=["name"], + ), + Chart( + "Total Sales [Pie]", + Chart.PIE, + data_source=["sum_of_value"], + title_source=["name"], + plot_total=True, + ), ] @@ -450,16 +466,18 @@ class TimeSeriesWithoutGroupBy(ReportView): ] chart_settings = [ - Chart("Total Sales [Bar]", - Chart.BAR, - data_source=["sum__value"], - title_source=["name"], - ), - Chart("Total Sales [Pie]", - Chart.PIE, - data_source=["sum__value"], - title_source=["name"], - ), + Chart( + "Total Sales [Bar]", + Chart.BAR, + data_source=["sum__value"], + title_source=["name"], + ), + Chart( + "Total Sales [Pie]", + Chart.PIE, + data_source=["sum__value"], + title_source=["name"], + ), ] @@ -473,7 +491,6 @@ class CrosstabReport(ReportView): "name", "__crosstab__", # You can customize where the crosstab columns are displayed in relation to the other columns - ComputationField.create(Sum, "value", verbose_name=_("Total Value")), # This is the same as the calculation in the crosstab, # but this one will be on the whole set. IE total value. @@ -501,7 +518,6 @@ class CrosstabWithIdsCustomFilter(CrosstabReport): report_title = _("Crosstab with Custom Filters") crosstab_ids_custom_filters = [ (~Q(product__size__in=["extra_big", "big"]), dict()), - (None, dict(product__size__in=["extra_big", "big"])), ] # Note: @@ -528,9 +544,7 @@ def get_crosstab_field_verbose_name(cls, model, id): class CrossTabReportWithCustomVerboseName(CrosstabReport): report_title = _("Crosstab with customized verbose name") - crosstab_columns = [ - CustomCrossTabTotalField - ] + crosstab_columns = [CustomCrossTabTotalField] class CustomCrossTabTotalField2(CustomCrossTabTotalField): @@ -550,16 +564,46 @@ def get_time_series_field_verbose_name(cls, date_period, index, dates, pattern): class CrossTabReportWithCustomVerboseNameCustomFilter(CrosstabWithIdsCustomFilter): report_title = _("Crosstab customized verbose name with custom filter") - crosstab_columns = [ - CustomCrossTabTotalField2 - ] + crosstab_columns = [CustomCrossTabTotalField2] class CrossTabWithTimeSeries(CrossTabReportWithCustomVerboseNameCustomFilter): report_title = _("Crosstab with time series") time_series_pattern = "monthly" - columns = [ - "name", - "__time_series__" + columns = ["name", "__time_series__"] + + +class ChartJSExample(TimeSeriesReport): + report_title = _("ChartJS Examples ") + + chart_engine = "chartsjs" + chart_settings = [ + Chart( + "Client Sales", + Chart.BAR, + data_source=["sum__value"], + title_source=["name"], + ), + Chart( + "Total Sales [Pie]", + Chart.PIE, + data_source=["sum__value"], + title_source=["name"], + plot_total=True, + ), + Chart( + "Total Sales [Line total]", + Chart.LINE, + data_source=["sum__value"], + title_source=["name"], + plot_total=True, + ), + Chart( + "Total Sales [Line details]", + Chart.LINE, + data_source=["sum__value"], + title_source=["name"], + # plot_total=True, + ), ] diff --git a/demo_proj/demo_proj/urls.py b/demo_proj/demo_proj/urls.py index d922256..1a8e1ff 100644 --- a/demo_proj/demo_proj/urls.py +++ b/demo_proj/demo_proj/urls.py @@ -17,67 +17,11 @@ from django.contrib import admin from django.urls import path -from demo_app.reports import ProductSales, TotalProductSales, TotalProductSalesByCountry, MonthlyProductSales, \ - ProductSalesPerCountryCrosstab, ProductSalesPerClientCrosstab, LastTenSales, TotalProductSalesWithCustomForm, \ - GroupByReport, GroupByTraversingFieldReport, GroupByCustomQueryset, TimeSeriesReport - -from demo_app import reports from demo_app import views +from demo_app import helpers -urlpatterns = [ - +urlpatterns = helpers.get_urls_patterns() + [ path("", views.HomeView.as_view(), name="home"), path("dashboard/", views.Dashboard.as_view(), name="dashboard"), - - - # tutorial - path("product-sales/", ProductSales.as_view(), name="product-sales"), - path("total-product-sales/", TotalProductSales.as_view(), name="total-product-sales"), - path("total-product-sales-by-country/", TotalProductSalesByCountry.as_view(), - name="total-product-sales-by-country"), - path("monthly-product-sales/", MonthlyProductSales.as_view(), name="monthly-product-sales"), - path("product-sales-per-client-crosstab/", ProductSalesPerClientCrosstab.as_view(), - name="product-sales-per-client-crosstab"), - path("product-sales-per-country-crosstab/", ProductSalesPerCountryCrosstab.as_view(), - name="product-sales-per-country-crosstab"), - path("last-10-sales/", LastTenSales.as_view(), name="last-10-sales"), - path("total-product-sales-with-custom-form/", TotalProductSalesWithCustomForm.as_view(), - name="total-product-sales-with-custom-form"), - - # Group by - path("group-by-report/", GroupByReport.as_view(), name="group-by-report"), - path("group-by-traversing-field/", GroupByTraversingFieldReport.as_view(), name="group-by-traversing-field"), - path("group-by-custom-queryset/", GroupByCustomQueryset.as_view(), name="group-by-custom-queryset"), - path("no-group-by/", reports.NoGroupByReport.as_view(), name="no-group-by"), - - # Time Series - path("time-series-report/", TimeSeriesReport.as_view(), name="time-series-report"), - path("time-series-with-selector/", reports.TimeSeriesReportWithSelector.as_view(), - name="time-series-with-selector"), - path("time-series-with-custom-dates/", reports.TimeSeriesReportWithCustomDates.as_view(), - name="time-series-with-custom-dates"), - path("time-series-with-custom-dates-and-title/", reports.TimeSeriesReportWithCustomDatesAndCustomTitle.as_view(), - name="time-series-with-custom-dates-and-title"), - path("time-series-without-group-by/", reports.TimeSeriesWithoutGroupBy.as_view(), - name="time-series-without-group-by"), - path("time-series-with-group-by-custom-queryset/", reports.TimeSeriesReportWithCustomGroupByQueryset.as_view(), - name="time-series-with-group-by-custom-queryset"), - - # Crosstab - path("crosstab-report/", reports.CrosstabReport.as_view(), name="crosstab-report"), - path("crosstab-report-with-ids/", reports.CrosstabWithIds.as_view(), name="crosstab-report-with-ids"), - path("crosstab-report-traversing-field/", reports.CrosstabWithTraversingField.as_view(), - name="crosstab-report-traversing-field"), - path("crosstab-report-custom-filter/", reports.CrosstabWithIdsCustomFilter.as_view(), - name="crosstab-report-custom-filter"), - path("crosstab-report-custom-verbose-name/", reports.CrossTabReportWithCustomVerboseName.as_view(), - name="crosstab-report-custom-verbose-name"), - path("crosstab-report-custom-verbose-name-2/", reports.CrossTabReportWithCustomVerboseNameCustomFilter.as_view(), - name="crosstab-report-custom-verbose-name-2"), - path("crosstab-report-with-time-series/", reports.CrossTabWithTimeSeries.as_view(), - name="crosstab-report-with-time-series"), - - - path("admin/", admin.site.urls), ] diff --git a/demo_proj/templates/menu.html b/demo_proj/templates/menu.html index a939f20..561dabd 100644 --- a/demo_proj/templates/menu.html +++ b/demo_proj/templates/menu.html @@ -163,4 +163,19 @@ + + \ No newline at end of file diff --git a/demo_proj/templates/slick_reporting/base.html b/demo_proj/templates/slick_reporting/base.html index 6932026..1389253 100644 --- a/demo_proj/templates/slick_reporting/base.html +++ b/demo_proj/templates/slick_reporting/base.html @@ -1,17 +1,9 @@ {% extends "base.html" %} - -{% block page_title %} - {{ report_title }} -{% endblock %} -{% block meta_page_title %} - {{ report_title }} - -{% endblock %} +{% block meta_page_title %} {{ report_title }}{% endblock %} +{% block page_title %} {{ report_title }} {% endblock %} {% block extrajs %} {{ block.super }} - {% include "slick_reporting/js_resources.html" %} - -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/docs/requirements.txt b/docs/requirements.txt index f0ffece..cad92e3 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,5 @@ -sphinx==4.2.0 -sphinx_rtd_theme==1.0.0 -readthedocs-sphinx-search==0.1.1 -django-slick-reporting==0.6.4 \ No newline at end of file +-r ../requirements.txt +crispy_bootstrap4 +sphinx +sphinx_rtd_theme==1.3.0 +readthedocs-sphinx-search==0.3.1 \ No newline at end of file diff --git a/docs/source/charts.rst b/docs/source/charts.rst deleted file mode 100644 index 3dafcef..0000000 --- a/docs/source/charts.rst +++ /dev/null @@ -1,114 +0,0 @@ -Charting and Front End Customization -===================================== - -Charts Configuration ---------------------- - -Charts settings is a list of objects which each object represent a chart configurations. - -* type: what kind of chart it is: Possible options are bar, pie, line and others subject of the underlying charting engine. - Hats off to : `Charts.js `_. -* engine_name: String, default to ``SLICK_REPORTING_DEFAULT_CHARTS_ENGINE``. Passed to front end in order to use the appropriate chart engine. - By default supports `highcharts` & `chartsjs`. -* data_source: Field name containing the numbers we want to plot. -* title_source: Field name containing labels of the data_source -* title: the Chart title. Defaults to the `report_title`. -* plot_total if True the chart will plot the total of the columns. Useful with time series and crosstab reports. - -On front end, for each chart needed we pass the whole response to the relevant chart helper function and it handles the rest. - - - - -The ajax response structure ---------------------------- - -Understanding how the response is structured is imperative in order to customize how the report is displayed on the front end - -Let's have a look - -.. code-block:: python - - - # Ajax response or `report_results` template context variable. - response = { - # the report slug, defaults to the class name all lower - "report_slug": "", - # a list of objects representing the actual results of the report - "data": [ - { - "name": "Product 1", - "quantity__sum": "1774", - "value__sum": "8758", - "field_x": "value_x", - }, - { - "name": "Product 2", - "quantity__sum": "1878", - "value__sum": "3000", - "field_x": "value_x", - }, - # etc ..... - ], - # A list explaining the columns/keys in the data results. - # ie: len(response.columns) == len(response.data[i].keys()) - # It contains needed information about verbose name , if summable and hints about the data type. - "columns": [ - { - "name": "name", - "computation_field": "", - "verbose_name": "Name", - "visible": True, - "type": "CharField", - "is_summable": False, - }, - { - "name": "quantity__sum", - "computation_field": "", - "verbose_name": "Quantities Sold", - "visible": True, - "type": "number", - "is_summable": True, - }, - { - "name": "value__sum", - "computation_field": "", - "verbose_name": "Value $", - "visible": True, - "type": "number", - "is_summable": True, - }, - ], - # Contains information about the report as whole if it's time series or a a crosstab - # And what's the actual and verbose names of the time series or crosstab specific columns. - "metadata": { - "time_series_pattern": "", - "time_series_column_names": [], - "time_series_column_verbose_names": [], - "crosstab_model": "", - "crosstab_column_names": [], - "crosstab_column_verbose_names": [], - }, - # A mirror of the set charts_settings on the ReportView - # ``ReportView`` populates the id and the `engine_name' if not set - "chart_settings": [ - { - "type": "pie", - "engine_name": "highcharts", - "data_source": ["quantity__sum"], - "title_source": ["name"], - "title": "Pie Chart (Quantities)", - "id": "pie-0", - }, - { - "type": "bar", - "engine_name": "chartsjs", - "data_source": ["value__sum"], - "title_source": ["name"], - "title": "Column Chart (Values)", - "id": "bar-1", - }, - ], - } - - diff --git a/docs/source/concept.rst b/docs/source/concept.rst index 33aa12f..4a3d9f6 100644 --- a/docs/source/concept.rst +++ b/docs/source/concept.rst @@ -1,37 +1,42 @@ .. _structure: -How the documentation is organized -================================== +Welcome to Django Slick Reporting documentation! +================================================== -:ref:`Tutorial ` --------------------------- +Django Slick Reporting a reporting engine allowing you to create and chart different kind of analytics from your model in a breeze. + +Demo site +--------- -If you are new to Django Slick Reporting, start here. It's a step-by-step guide to building a simple report(s). +If you haven't yet, please check https://django-slick-reporting.com for a quick walk-though with live code examples.. -:ref:`How-to guides ` ------------------------------ -Practical, hands-on guides that show you how to achieve a specific goal with Django Slick Reporting. Like customizing the form, creating a computation field, etc. +:ref:`Tutorial ` +-------------------------- + +The tutorial will guide you to what is slick reporting, what kind of reports it can do for you and how to use it in your project. + :ref:`Topic Guides ` ---------------------------- -Discuss each type of reports you can create with Django Slick Reporting and their options. +Discuss each type of report main structures you can create with Django Slick Reporting and their options. - * :ref:`Grouped report `: Similar to what we'd do with a GROUP BY sql statement. We group by a field and do some kind of calculations over the grouped records. - * :ref:`time_series`: A step up from the grouped report, where the calculations are computed for each time period (day, week, month, etc). - * :ref:`crosstab_reports`: Where the results shows the relationship between two or more variables. Example: Rows are the clients, columns are the products, and the intersection values are the sum of sales for each client and product combination. This report can be created in time series as well. Example: Rows are the clients, columns are the products, and the intersection values are the sum of sales for each client and product combination, for each month. - * :ref:`list_reports`: Similar to a django changelist, it's a direct view of the report model records with some extra features like sorting, filtering, pagination, etc. + * :ref:`Group By report `: Similar to what we'd do with a GROUP BY sql statement. We group by a field and do some kind of calculations over the grouped records. + * :ref:`time_series`: A step further, where the calculations are computed for time periods (day, week, month, custom etc). + * :ref:`crosstab_reports`: Where the results shows the relationship between two or more variables. It's a table that shows the distribution of one variable in rows and another in columns. + * :ref:`list_reports`: Similar to a django admin's changelist, it's a direct view of the report model records * And other topics like how to customize the form, and extend the exporting options. :ref:`Reference ` ---------------------------- -Detailed information about main on Django Slick Reporting's main components, such as the :ref:`Report View `, :ref:`Generator `, :ref:`Computation Field `, etc. +Detailed information about main on Django Slick Reporting's main components + #. :ref:`Settings `: The settings you can use to customize the behavior of Django Slick Reporting. #. :ref:`Report View `: A ``FormView`` CBV subclass with reporting capabilities allowing you to create different types of reports in the view. It provide a default :ref:`Filter Form ` to filter the report on. It mimics the Generator API interface, so knowing one is enough to work with the other. @@ -46,8 +51,3 @@ Detailed information about main on Django Slick Reporting's main components, suc - -Demo site ---------- - -If you haven't yet, please check https://django-slick-reporting.com for a quick walk-though with live code examples.. diff --git a/docs/source/howto/override_filter_form.rst b/docs/source/howto/override_filter_form.rst deleted file mode 100644 index e69de29..0000000 diff --git a/docs/source/index.rst b/docs/source/index.rst index 49882cc..37a577f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -90,8 +90,6 @@ Next step :ref:`tutorial` concept tutorial topics/index - howto/index - charts ref/index diff --git a/docs/source/ref/index.rst b/docs/source/ref/index.rst index 8d5f939..f81e27b 100644 --- a/docs/source/ref/index.rst +++ b/docs/source/ref/index.rst @@ -9,6 +9,7 @@ Below are links to the reference documentation for the various components of the :maxdepth: 2 :caption: Components: + settings computation_field report_generator view_options diff --git a/docs/source/ref/settings.rst b/docs/source/ref/settings.rst index 20396bd..f3036be 100644 --- a/docs/source/ref/settings.rst +++ b/docs/source/ref/settings.rst @@ -1,7 +1,73 @@ +.. _ settings: Settings ======== +.. note:: + + Settings are changed in version 1.1.1 to being a dictionary instead of individual variables. + Variables will continue to work till next major release. + + +Below are the default settings for django-slick-reporting. You can override them in your settings file. + +.. code-block:: python + + SLICK_REPORTING_SETTINGS = { + "JQUERY_URL": "https://code.jquery.com/jquery-3.7.0.min.js", + "DEFAULT_START_DATE_TIME": datetime( + datetime.now().year, 1, 1, 0, 0, 0, tzinfo=timezone.utc + ), # Default: 1st Jan of current year + "DEFAULT_END_DATE_TIME": datetime.datetime.today(), # Default to today + "FONT_AWESOME": { + "CSS_URL": "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css", + "ICONS": { + "pie": "fas fa-chart-pie", + "bar": "fas fa-chart-bar", + "line": "fas fa-chart-line", + "area": "fas fa-chart-area", + "column": "fas fa-chart-column", + }, + }, + "CHARTS": { + "highcharts": "$.slick_reporting.highcharts.displayChart", + "chartjs": "$.slick_reporting.chartjs.displayChart", + }, + "MESSAGES": { + "total": _("Total"), + }, + } + +* JQUERY_URL: + + Link to the jquery file, You can use set it to False and manage the jQuery addition to your liking + +* DEFAULT_START_DATE_TIME + + Default date time that would appear on the filter form in the start date + +* DEFAULT_END_DATE_TIME + + Default date time that would appear on the filter form in the end date + +* FONT_AWESOME: + + Font awesome is used to display the icon next to the chart title. You can override the following settings: + + 1. ``CSS_URL``: URL to the font-awesome css file + 2. ``ICONS``: Icons used for different chart types. + +* CHARTS: + + The entry points for displaying charts on the front end. + You can add your own chart engine by adding an entry to this dictionary. + +* MESSAGES: + + The strings used in the front end. You can override them here, it also gives a chance to set and translate them per your requirements. + + +Old versions settings: 1. ``SLICK_REPORTING_DEFAULT_START_DATE``: Default: the beginning of the current year 2. ``SLICK_REPORTING_DEFAULT_END_DATE``: Default: the end of the current year. diff --git a/docs/source/topics/filter_form.rst b/docs/source/topics/filter_form.rst index 2e540ef..a68e737 100644 --- a/docs/source/topics/filter_form.rst +++ b/docs/source/topics/filter_form.rst @@ -120,4 +120,4 @@ Example a full example of a custom form: class RequestCountByPath(ReportView): form_class = RequestLogForm -You can view this code snippet in action on the demo project https://my-shop.django-erp-framework.com/requests-dashboard/reports/request_analytics/requestlog/ +You can view this code snippet in action on the demo project https://django-slick-reporting.com/total-product-sales-with-custom-form/ diff --git a/docs/source/topics/index.rst b/docs/source/topics/index.rst index 5b2f26f..3cfc7e2 100644 --- a/docs/source/topics/index.rst +++ b/docs/source/topics/index.rst @@ -31,6 +31,6 @@ You saw how to use the ReportView class in the tutorial and you identified the t crosstab_options list_report_options filter_form + widgets + integrating_slick_reporting exporting - - diff --git a/docs/source/topics/integrating_slick_reporting.rst b/docs/source/topics/integrating_slick_reporting.rst new file mode 100644 index 0000000..a14b370 --- /dev/null +++ b/docs/source/topics/integrating_slick_reporting.rst @@ -0,0 +1,82 @@ +Integrating reports into your front end +======================================= + +To integrate Slick Reporting into your application, you need to do override "slick_reporting/base.html" template, +and/or, for more fine control over the report layout, override "slick_reporting/report.html" template. + +Example 1: Override base.html + +.. code-block:: html+django + + {% extends "base.html" %} + + {% block meta_page_title %} {{ report_title }}{% endblock %} + {% block page_title %} {{ report_title }} {% endblock %} + + {% block extrajs %} + {{ block.super }} + {% include "slick_reporting/js_resources.html" %} + {% endblock %} + + + +Let's see what we did there +1. We made our slick_reporting/base.html extend the main base.html +2. We added the ``report_title`` context variable (which hold the current report title) to the meta_page_title and page_title blocks. + Use your version of these blocks, you might have them named differently. +3. We added the slick_reporting/js_resources.html template to the extrajs block. This template contains the javascript resources needed for slick_reporting to work. + Also, use your version of the extrajs block. You might have it named differently. + +And that's it ! You can now use slick_reporting in your application. + + +Example 2: Override report.html + +Maybe you want to add some extra information to the report, or change the layout of the report. +You can do this by overriding the slick_reporting/report.html template. + +Here is how it looks like: + +.. code-block:: html+django + + {% extends 'slick_reporting/base.html' %} + {% load crispy_forms_tags i18n %} + + {% block content %} +
+ {% if form %} +
+
+

{% trans "Filters" %}

+
+
+ {% crispy form crispy_helper %} +
+ +
+ {% endif %} + +
+
+
{% trans "Results" %}
+
+
+
+
+
+
+
+
+
+
+
+ {% endblock %} diff --git a/docs/source/topics/list_report_options.rst b/docs/source/topics/list_report_options.rst index f424f23..c4ac74c 100644 --- a/docs/source/topics/list_report_options.rst +++ b/docs/source/topics/list_report_options.rst @@ -7,25 +7,42 @@ List Reports It's a simple ListView / admin changelist like report to display data in a model. It's quite similar to ReportView except there is no calculation by default. -Options: --------- +Here is the options you can use to customize the report: -filters: a list of report_model fields to be used as filters. +#. ``columns``: a list of report_model fields to be displayed in the report, which support traversing .. code-block:: python class RequestLog(ListReportView): - report_model = Request + report_model = SalesTransaction + columns = [ + "id", + "date", + "client__name", + "product__name", + "quantity", + "price", + "value", + ] - filters = ["method", "path", "user_agent", "user", "referer", "response"] +#. ``filters``: a list of report_model fields to be used as filters. -Would yield a form like this +.. code-block:: python + + class RequestLog(ListReportView): + report_model = SalesTransaction + columns = [ + "id", + "date", + "client__name", + "product__name", + "quantity", + "price", + "value", + ] + + filters = ["product", "client"] -.. image:: _static/list_view_form.png - :width: 800 - :alt: ListReportView form - :align: center -Check :ref:`filter_form_customization` To customize the form as you wish. diff --git a/docs/source/topics/widgets.rst b/docs/source/topics/widgets.rst index 66f3312..fac5f80 100644 --- a/docs/source/topics/widgets.rst +++ b/docs/source/topics/widgets.rst @@ -34,13 +34,15 @@ You can pass arguments to the ``get_widget`` function to control aspects of its * success_callback: string, the name of a javascript function that will be called after the report data is retrieved. * failure_callback: string, the name of a javascript function that will be called if the report data retrieval fails. * template_name: string, the template name used to render the widget. Default to `slick_reporting/widget_template.html` +* extra_params: string, extra parameters to pass to the report. +* report_form_selector: string, a jquery selector that will be used to find the form that will be used to pass extra parameters to the report. This code above will be actually rendered as this in the html page: .. code-block:: html+django -
+
-
+
+
-
@@ -65,49 +67,10 @@ This code above will be actually rendered as this in the html page: The ``data-report-widget`` attribute is used by the javascript to find the widget and render the report. -you can add [data-no-auto-load] to the widget to prevent the widget from loading automatically. - -The ``data-report-url`` attribute is the url that will be used to fetch the data. -The ``data-extra-params`` attribute is used to pass extra parameters to the report. -The ``data-success-callback`` attribute is used to pass a javascript function that will be called after -the report data is retrieved. -The ``data-fail-callback`` attribute is used to pass a javascript function -that will be called if the report data retrieval fails. -The ``report-form-selector`` attribute is used to pass a jquery selector -that will be used to find the form that will be used to pass extra parameters -to the report. -The ``data-chart-id`` attribute is used to pass the id of the chart that will -be rendered. The ``data-display-chart-selector`` attribute is used to pass -if the report loader should display the chart selectors links. - - -The ``data-report-chart`` attribute is used by the javascript to find the -container for the chart. The ``data-report-table`` attribute is used by the -javascript to find the container for the table. - - -``get_widget`` Tag can accept a ``template_name`` parameter to render the -report using a custom template. By default it renders the -``erp_reporting/report_widget.html`` template. - -Default Arguments ------------------ - -extra_params -success_callback -failure_callback -display_chart -display_table -chart_id -display_title -title (default to report report title) - - - - +you can add [data-no-auto-load] to the widget to prevent report loader to get the widget data automatically. -Customization -------------- +Customization Example +--------------------- You You can customize how the widget is loading by defining your own success call-back and fail call-back functions. @@ -117,12 +80,9 @@ The success call-back function will receive the report data as a parameter .. code-block:: html+django - {% load i18n static erp_reporting_tags %} + {% load i18n static slick_reporting_tags %} -
- {% get_report base_model='expense' report_slug='ExpensesTotalStatement' as ExpensesTotalStatement %} - {% get_html_panel ExpensesTotalStatement data-success-callback='my_success_callback' %} -
+ {% get_widget_from_url url_name="product-sales" success_callback=my_success_callback %} + {% endblock %} - - {{ report_title }} | Django Slick Reporting diff --git a/slick_reporting/templates/slick_reporting/js_resources.html b/slick_reporting/templates/slick_reporting/js_resources.html index 7b200dc..67b51f3 100644 --- a/slick_reporting/templates/slick_reporting/js_resources.html +++ b/slick_reporting/templates/slick_reporting/js_resources.html @@ -1,18 +1,20 @@ -{% load i18n static %} +{% load i18n static slick_reporting_tags %} +{% get_slick_reporting_settings as slick_reporting_settings %} - - +{% add_jquery %} + - - + + + + href="{{ slick_reporting_settings.FONT_AWESOME.CSS_URL }}"/> @@ -20,11 +22,11 @@ - + - +{{ slick_reporting_settings|json_script:"slick_reporting_settings" }} diff --git a/slick_reporting/templates/slick_reporting/report.html b/slick_reporting/templates/slick_reporting/report.html index 8a11c0f..51d7903 100644 --- a/slick_reporting/templates/slick_reporting/report.html +++ b/slick_reporting/templates/slick_reporting/report.html @@ -1,6 +1,5 @@ {% extends 'slick_reporting/base.html' %} -{% load crispy_forms_tags i18n slick_reporting_tags static %} - +{% load crispy_forms_tags i18n %} {% block content %}
@@ -16,7 +15,6 @@

{% trans "Filters" %}

-
{% endif %} @@ -26,7 +24,6 @@

{% trans "Filters" %}

{% trans "Results" %}
-
{% trans "Results" %}
-
- - {% endblock %} diff --git a/slick_reporting/templates/slick_reporting/table.html b/slick_reporting/templates/slick_reporting/table.html deleted file mode 100644 index 4c902ea..0000000 --- a/slick_reporting/templates/slick_reporting/table.html +++ /dev/null @@ -1,20 +0,0 @@ -{% load slick_reporting_tags %} - - - - {% for column in table.columns %} - - - {% endfor %} - - - -{% for row in table.data %} - - {% for column in table.columns %} - - {% endfor %} - -{% endfor %} - -
{{ column.verbose_name }}
{% get_data row column %}
\ No newline at end of file diff --git a/slick_reporting/templatetags/slick_reporting_tags.py b/slick_reporting/templatetags/slick_reporting_tags.py index a985e91..543787e 100644 --- a/slick_reporting/templatetags/slick_reporting_tags.py +++ b/slick_reporting/templatetags/slick_reporting_tags.py @@ -1,14 +1,10 @@ -import simplejson as json - from django import template -from django.core.serializers import serialize -from django.db.models import QuerySet from django.template.loader import get_template from django.urls import reverse, resolve -from django.utils.encoding import force_str -from django.utils.functional import Promise from django.utils.safestring import mark_safe +from slick_reporting.app_settings import SLICK_REPORTING_JQUERY_URL + register = template.Library() @@ -17,20 +13,21 @@ def get_data(row, column): return row[column["name"]] -def jsonify(object): - def date_handler(obj): - if hasattr(obj, "isoformat"): - return obj.isoformat() - elif isinstance(obj, Promise): - return force_str(obj) - - if isinstance(object, QuerySet): - return serialize("json", object) - - return mark_safe(json.dumps(object, use_decimal=True, default=date_handler)) - - -register.filter("jsonify", jsonify) +# +# def jsonify(object): +# def date_handler(obj): +# if hasattr(obj, "isoformat"): +# return obj.isoformat() +# elif isinstance(obj, Promise): +# return force_str(obj) +# +# if isinstance(object, QuerySet): +# return serialize("json", object) +# +# return mark_safe(json.dumps(object, use_decimal=True, default=date_handler)) +# +# +# register.filter("jsonify", jsonify) @register.simple_tag @@ -72,3 +69,17 @@ def get_widget(report, template_name="", url_name="", report_url=None, **kwargs) template = get_template(template_name or "slick_reporting/widget_template.html") return template.render(context=kwargs) + + +@register.simple_tag +def add_jquery(): + if SLICK_REPORTING_JQUERY_URL: + return mark_safe(f'') + return "" + + +@register.simple_tag +def get_slick_reporting_settings(): + from slick_reporting.app_settings import SLICK_REPORTING_SETTINGS + + return SLICK_REPORTING_SETTINGS diff --git a/slick_reporting/views.py b/slick_reporting/views.py index c1115b3..d497528 100644 --- a/slick_reporting/views.py +++ b/slick_reporting/views.py @@ -12,10 +12,7 @@ from django.utils.functional import Promise from django.views.generic import FormView -from .app_settings import ( - SLICK_REPORTING_DEFAULT_END_DATE, - SLICK_REPORTING_DEFAULT_START_DATE, -) +from .app_settings import SLICK_REPORTING_SETTINGS from .forms import ( report_form_factory, get_crispy_helper, @@ -113,6 +110,7 @@ class ReportViewBase(ReportGeneratorAPI, FormView): doc_type_plus_list = None doc_type_minus_list = None auto_load = True + chart_engine = "" default_order_by = "" @@ -220,7 +218,7 @@ def get_form_class(self): display_compute_remainder=self.crosstab_compute_remainder, excluded_fields=self.excluded_fields, fkeys_filter_func=self.form_filter_func, - initial=self.get_form_initial(), + initial=self.get_initial(), 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, @@ -345,6 +343,7 @@ def get_report_results(self, for_print=False): report_slug=self.get_report_slug(), chart_settings=self.chart_settings, default_chart_title=self.report_title, + default_chart_engine=self.chart_engine, ) @classmethod @@ -359,7 +358,7 @@ def get_chart_settings(self, generator): """ Ensure the sane settings are passed to the front end. """ - return generator.get_chart_settings(self.chart_settings or [], self.report_title) + return generator.get_chart_settings(self.chart_settings or [], self.report_title, self.chart_engine) @classmethod def get_queryset(cls): @@ -381,12 +380,10 @@ def filter_results(self, data, for_print=False): def get_report_slug(cls): return cls.report_slug or cls.__name__.lower() - @staticmethod - def get_form_initial(): - # todo revise why not actually displaying datetime on screen + def get_initial(self): return { - "start_date": SLICK_REPORTING_DEFAULT_START_DATE, - "end_date": SLICK_REPORTING_DEFAULT_END_DATE, + "start_date": SLICK_REPORTING_SETTINGS["DEFAULT_START_DATE_TIME"], + "end_date": SLICK_REPORTING_SETTINGS["DEFAULT_END_DATE_TIME"], } def get_form_crispy_helper(self): diff --git a/tests/report_generators.py b/tests/report_generators.py index a3445e9..89d3172 100644 --- a/tests/report_generators.py +++ b/tests/report_generators.py @@ -1,6 +1,6 @@ import datetime -from django.db.models import Sum +from django.db.models import Sum, Count from django.utils.translation import gettext_lazy as _ from slick_reporting.fields import ComputationField, PercentageToBalance @@ -44,11 +44,7 @@ class CrosstabOnClient(GenericGenerator): columns = ["name", "__total_quantity__"] crosstab_field = "client" # crosstab_columns = ['__total_quantity__'] - crosstab_columns = [ - ComputationField.create( - Sum, "quantity", name="value__sum", verbose_name=_("Sales") - ) - ] + crosstab_columns = [ComputationField.create(Sum, "quantity", name="value__sum", verbose_name=_("Sales"))] class CrosstabTimeSeries(GenericGenerator): @@ -75,11 +71,7 @@ class CrosstabOnField(ReportGenerator): crosstab_field = "flag" crosstab_ids = ["sales", "sales-return"] - crosstab_columns = [ - ComputationField.create( - Sum, "quantity", name="value__sum", verbose_name=_("Sales") - ) - ] + crosstab_columns = [ComputationField.create(Sum, "quantity", name="value__sum", verbose_name=_("Sales"))] class CrosstabCustomQueryset(ReportGenerator): @@ -96,11 +88,7 @@ class CrosstabCustomQueryset(ReportGenerator): (None, dict(flag="sales-return")), ] - crosstab_columns = [ - ComputationField.create( - Sum, "quantity", name="value__sum", verbose_name=_("Sales") - ) - ] + crosstab_columns = [ComputationField.create(Sum, "quantity", name="value__sum", verbose_name=_("Sales"))] class CrosstabOnTraversingField(ReportGenerator): @@ -113,11 +101,7 @@ class CrosstabOnTraversingField(ReportGenerator): crosstab_field = "client__sex" crosstab_ids = ["FEMALE", "MALE", "OTHER"] - crosstab_columns = [ - ComputationField.create( - Sum, "quantity", name="value__sum", verbose_name=_("Sales") - ) - ] + crosstab_columns = [ComputationField.create(Sum, "quantity", name="value__sum", verbose_name=_("Sales"))] class ClientTotalBalance(ReportGenerator): @@ -214,10 +198,6 @@ class ProductTotalSalesWithPercentage(ReportGenerator): class ClientList(ReportGenerator): - report_title = _("Our Clients") - - # report_slug = 'client_list' - base_model = Client report_model = SimpleSales group_by = "client" @@ -315,6 +295,21 @@ class ClientSalesMonthlySeries(ReportGenerator): time_series_columns = ["__debit__", "__credit__", "__balance__", "__total__"] +class CountField(ComputationField): + calculation_field = "id" + calculation_method = Count + verbose_name = _("Count") + name = "count__id" + + +class TestCountField(ReportGenerator): + report_model = ComplexSales + + group_by = "product" + columns = ["slug", "name", CountField] + date_field = "doc_date" + + # @@ -387,11 +382,7 @@ class ProductClientSalesMatrix2(ReportGenerator): columns = ["slug", "name"] crosstab_field = "client" - crosstab_columns = [ - ComputationField.create( - Sum, "value", name="value__sum", verbose_name=_("Sales") - ) - ] + crosstab_columns = [ComputationField.create(Sum, "value", name="value__sum", verbose_name=_("Sales"))] class ProductClientSalesMatrixwSimpleSales2(ReportGenerator): @@ -402,11 +393,7 @@ class ProductClientSalesMatrixwSimpleSales2(ReportGenerator): columns = ["slug", "name"] crosstab_field = "client" - crosstab_columns = [ - ComputationField.create( - Sum, "value", name="value__sum", verbose_name=_("Sales") - ) - ] + crosstab_columns = [ComputationField.create(Sum, "value", name="value__sum", verbose_name=_("Sales"))] class GeneratorClassWithAttrsAs(ReportGenerator): diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..bfca5c0 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,2 @@ +-r ../requirements.txt +crispy-bootstrap4 \ No newline at end of file diff --git a/tests/test_generator.py b/tests/test_generator.py index 051ef15..3d1401e 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -19,6 +19,7 @@ CrosstabOnField, CrosstabOnTraversingField, CrosstabCustomQueryset, + TestCountField, ) from .tests import BaseTestData, year @@ -422,13 +423,20 @@ def test_group_by_char_field(self): # test that columns are a straight forward list -class TestReportFields(TestCase): +class TestReportFields(BaseTestData, TestCase): def test_get_full_dependency_list(self): from slick_reporting.fields import BalanceReportField deps = BalanceReportField.get_full_dependency_list() self.assertEqual(len(deps), 1) + def test_computation_field_count(self): + # test case for issue #77 + report = TestCountField() + data = report.get_report_data() + self.assertEqual(data[0]["count__id"], 5) + self.assertEqual(data[1]["count__id"], 1) + class TestHelpers(TestCase): def test_get_model_for_keys(self):