Metadata-Version: 2.1
Name: django-simple-stats
Version: 0.4.0
Summary: A django package for creating simple stats from a query
Home-page: https://github.com/spapas/django-simple-stats
Author: Serafeim Papastefanos
Author-email: spapas@gmail.com
License: MIT
Classifier: Environment :: Web Environment
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.6
Classifier: Framework :: Django
Classifier: Framework :: Django :: 3.0
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries
License-File: LICENSE

.. image:: https://badge.fury.io/py/django-simple-stats.svg
    :target: https://badge.fury.io/py/django-simple-stats
    
django-simple-stats
-------------------

A django package for creating stats from a query. 

installation
============

Install it from pip:

``pip install django-simple-stats``

or the latest version from git:

``pip install git+https://github.com/spapas/django-simple-stats``

No other installation is needed.


Usage
=====

The only supported method is the ``get_stats``. It expects a django query and a configuration list. 
Each element of the configuration list is a dictionary with the following attributes:

* label (required): The textual description of this statistic
* kind (required): What kind of aggregate we need. Choices are: ``query_aggregate_single``, ``query_aggregate``, ``choice_aggregate``, ``choice_aggregate_with_null``, ``query_aggregate_date``, ``query_aggregate_buckets``. 
* method (required): The aggregate method. Can be one of ``count``, ``sum``, ``max``, ``min``, ``avg``.
* field (required): The field that the aggreate will run on; use ``__`` for joins i.e ``fiedld1__field2``
* what (optional): Only required for ``query_aggregate_date``, it is eithed ``year``, ``month``, ``day``
* choices (optional): Only required for ``choice_aggregate`` and ``choice_aggregate_with_null``, it must be a django choices list 
* buckets (optional): only required for ``query_aggregate_buckets``. Must be a list from the biggest to the lowest value.
* aggr_field (optional): this field is optional and can be used for ``query_aggregate``, ``query_aggregate_date``, ``choice_aggregate`` and ``choice_aggregate_with_null``. It denotes a field that would run the aggregate function on.

See below for a complete example.

The response will be a list of dictionaries with the following attributes:

* label: Same as the label in the configuration
* value: Will have a value if you use the query_aggregate_single, else will be None 
* values: Will be empty for query_aggregate_single else will be a list of tuples. Each tuple will have two elements, ``(label, value)``

Stat kinds
==========

* The ``query_aggregate_single`` will run the aggregate function on a field and return a single value. For example you can get the total  number of rows of your query or the sum of all fields. 

* The ``query_aggregate`` will run the aggregate function on a field and return the list of values. You can run the aggregate function *on a different field* by passing ``aggr_field`` (so you can group by a field and return the sum of another field for each group). This is mainly useful for foreign keys and if you've got distinct values in your queries. For example count the number of rows per user. Also it is useful for booleans for example to get the number of rows that have a flag turned on and off. 

* The ``choice_aggregate`` is similar to the ``query_aggregate`` but will use a ``choices`` attribute to return better looking values. This will not return Null values

* The ``choice_aggregate_with_null`` is the same as ``choice_aggregate`` but will return Null values (so you can add a ``(None, "Empty")`` choice to your choices)

* The ``query_aggregate_date`` is similar to the ``query_aggregate`` but will return the aggregates on a specific date field; use ``what`` to pass ``year``, ``month``, ``day``.

* Finally, the ``query_aggregate_buckets`` is used to create buckets of values. You'll pass the list of buckets and the query will  return the results that belong in each bucket. The stats module will run individual queries with ``field__gte`` for each value. So for example if you pass ``[100, 50, 10]`` and you have a field ``price`` it will run ``price__gte=100``, ``price__gte=50``, ``price__gte=10`` and return the results.

Example
=======

.. code-block:: python

    from simple_stats import get_stats

    STATS_CFG = cfg = [
            {
                'label': 'Total',
                'kind': 'query_aggregate_single',
                'method': 'count',
                'field': 'id',
            },
            {
                'label': 'Total price',
                'kind': 'query_aggregate_single',
                'method': 'sum',
                'field': 'price',
            },
            {
                'label': 'Per authority',
                'kind': 'query_aggregate',
                'method': 'count',
                'field': 'pilot_authority__name',
            },
            {
                'label': 'Per authority by price',
                'kind': 'query_aggregate',
                'method': 'count',
                'field': 'pilot_authority__name',
                'aggr_field': 'price',
            },
            {
                'label': 'Per status',
                'kind': 'choice_aggregate',
                'method': 'count',
                'field': 'status',
                'choices': models.STATUS_CHOICES,
            },
            {
                'label': 'Per status by price',
                'kind': 'choice_aggregate',
                'method': 'count',
                'field': 'status',
                'aggr_field': 'price',
                'choices': models.STATUS_CHOICES,
            },
            {
                'label': 'Per year',
                'kind': 'query_aggregate_date',
                'method': 'count',
                'field': 'created_on',
                'what': 'year',
            },
            {
                'label': 'Per year by price',
                'kind': 'query_aggregate_date',
                'method': 'count',
                'field': 'created_on',
                'what': 'year',
                'aggr_field': 'price',
            },
            {
                'label': 'Per price',
                'kind': 'query_aggregate_buckets',
                'method': 'count',
                'field': 'price',
                'buckets': [100_00, 50_00, 1_000, 500, 0]
            }
        ]

    def my_view(request):
        qs = TestModel.objects.all()

        stats = get_stats(qs, STATS_CFG)
        return render(request, 'my_template.html', {'stats': stats})

The ``stats`` will be an array of dictionaries like the following:

.. code-block:: python

  [
    {'label': 'Total', 'values': [], 'value': 1216}, 
    {'label': 'Total price', 'values': [], 'value': 323.16}, 
    {'label': 'Per authority', 'values': [('Authority 1', 200), ('Authority 2', 9),   ], 'value': None}, 
    {'label': 'Per authority by price', 'values': [('Authority 1', 123.23), ('Authority 2', 42.12),   ], 'value': None}, 
    {'label': 'Per status', 'values': [('New', 200), ('Cancel', 0), 'value': None},
    {'label': 'Per status by price', 'values': [('New', 32.01), ('Cancel', 44.23), 'value': None},
    {'label': 'Per year', 'values': [(2021, 582), (2022, 634)], 'value': None}
    {'label': 'Per year by price', 'values': [(2021, 5.82), (2022, 6.34)], 'value': None}
    {'label': 'Per price', 'values': [('> 5000', 1), ('> 1000', 29), ('> 500', 86), ('> 0', 305)], 'value': None}
  ]
  
You can display this in your template using something like:

.. code-block:: python

  <div class='row'>
    {% for s in stats %}
    <div class='col-md-4 mb-5' style='max-height: 500px; overflow: auto;'>
        <h4>{{ s.label }}</h4>
        {% if s.values %}
            <table class='table table-condensed table-striped small table-sm'>
                {% for v in s.values %}
                    <tr>
                        <td>{{ v.0 }}</td>
                        <td>{{ v.1 }}</td>
                    </tr>
                {% endfor %}
            </table>
        {% else %}
            <b>{{ s.value }}</b>
        {% endif %}
    </div>
    {% endfor %}
  </div>

Exporting the stats
===================

You can easily export these stats in xls using the xlwt (https://pypi.org/project/xlwt/) library and this function:

.. code-block:: python

    import xlwt
    
    def create_xls_resp(stats, response):
        context = self.get_context_data()
        import xlwt
        wb = xlwt.Workbook(encoding="utf-8")
        for stat in stats:
            ws = wb.add_sheet(stat["label"][:31])
            ws.write(0,0,stat["label"], xlwt.easyxf('font: name Calibri, bold on', ))
            if stat["value"]:
                ws.write(0,1,stat["value"], xlwt.easyxf('font: name Calibri, bold on', ))

            for i, val in enumerate(stat["values"], start=2):
                for j,v in enumerate(val, start=0):
                    ws.write(i,j,v)
        wb.save(response)

Now you can call it like this from your view:

.. code-block:: python

    from django.http import HttpResponse

    def my_export_view(request):
        qs = TestModel.objects.all()

        stats = get_stats(qs, STATS_CFG)
        response = HttpResponse(content_type="application/ms-excel")
        response["Content-Disposition"] = "attachment; filename=export.xls"
        create_xls_resp(response)
        return response
            

Changelog
=========

* v.0.4.0: Allow the aggregate function to run on a different field using ``aggr_field``
* v.0.3.1: Fix small bug with ``choice_aggregate_with_null``
* v.0.3.0: Add ``choice_aggregate_with_null`` and throw if stat kind is not found
* v.0.2.1: Fix small bug with column aliases
* v.0.2.0: Changed API; use ``query_aggregate_datetime`` for a datetime field and ``query_aggregate_date`` for a date field
* v.0.1.0: Initial version
