Package: django-simple-stats
django-simple-statsA django package for creating stats from a query.
This package should be compatible with all
Django versions > 3.xYou can use something like this:classTravelStats(StatSet):total=QueryAggregateSingleStat(label='Total')amount=QueryAggregateSingleStat(label='Total amount',method='sum')port__name=QueryAggregateStat(label='By port',)to get something like this:InstallationInstall it from pip:pip installdjango-simple-statsor the latest version from git:pip installgit+https://github.com/spapas/django-simple-statsNo other installation is needed.Stat kindsThequery_aggregate_single/QueryAggregateSingleStatwill 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.Thequery_aggregate/QueryAggregateStatwill run the aggregate function on a field and return the list of values. You can run the aggregate functionon a different fieldby passingaggr_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.Thechoice_aggregate/ChoiceAggregateStatis similar to thequery_aggregatebut will use achoicesattribute to return better looking values. This will not return Null valuesThechoice_aggregate_with_null/ChoiceAggregateNullStatis the same aschoice_aggregatebut will return Null values (so you can add a(None, "Empty")choice to your choices)Thequery_aggregate_date/QueryAggregateDateStatis similar to thequery_aggregatebut will return the aggregates on a specific date field; usewhatto passyear,month,day.Thequery_aggregate_datetime/QueryAggregateDateTimeStatis similar to thequery_aggregate_datebut will return the aggregates on time also.Thequery_aggregate_extract_date/QueryAggregateExtractDateStatis similar toquery_aggregate_datebut will useExtractfor the date instead ofTrunc. This is useful if you want to group by the month/day/hour etc as aspecificvalue, i.e this will group all rows of June on the same row whilequery_aggregate_datewill differntiate between June 21 and June 22 and June 23.Finally, thequery_aggregate_buckets//QueryAggregateBucketsStatis 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 withfield__gtefor each value. So for example if you pass[100, 50, 10]and you have a fieldpriceit will runprice__gte=100,price__gte=50,price__gte=10and return the results.UsageBoth a declarative (similar to how Django models/forms work) and a functional API can be used.For the declarative API, you need to declare a class inheriting fromsimple_stats.StatSetand
declare the stats fields in it using the corresponding stats classes likesimple_stats.QueryAggregateStat.
Then you’ll initialize this class in your Django view passing it the query. For example:fromsimple_statsimportStatSet,QueryAggregateSingleStatclassMyStats(StatSet):id=QueryAggregateSingleStat(label='Total number')# Then in your viewstats=MyStats(query=MyModel.objects.all())The stats classes take the following initialization parameters:field (optional): The field that the aggreate will run on; use__for joins i.efiedld1__field2label (optional): The textual description of this statistic (will be filled with the field if not passed)method (optional): The aggregate method. Can be one ofcount,sum,max,min,avg. Default is countwhat (optional): Only required forquery_aggregate_date, it is eithedyear,month,daychoices (optional): Only required forchoice_aggregateandchoice_aggregate_with_null, it must be a django choices listbuckets (optional): Only required forquery_aggregate_buckets. Must be a list from the biggest to the lowest value.aggr_field (optional): This field is optional and can be used forquery_aggregate,query_aggregate_date,choice_aggregateandchoice_aggregate_with_null. It denotes a field that would run the aggregate function on.formatter (optional): A callback that can be used to format the value, it should get a value and return a rendered value (i.elambda v:"${}".format(v))Please notice that if you don’t pass thefieldparameter then the name of the attribute will be used (i.e. it will befield=idin the example above).TheStatSetinstance will be an enumerable returning a list of dictionaries with the following attributes:label: Same as the label in the configurationvalue: Will have a value if you use the query_aggregate_single, else will be Nonevalues: Will be empty for query_aggregate_single else will be a list of tuples. Each tuple will have two elements,(label, value)On the other hand, the only supported method in the functional API thesimple_stats.get_stats.
It expects a django query and the stats configuration (list of dicts).
Each element of the configuration list is a dictionary that has the same attributes as the init parameters
for the class. There are only two differences:The field is now requiredWe must pass the kind of aggregate we need (similar to the class we used on the declarative API). Choices here are:query_aggregate_single,query_aggregate,choice_aggregate,choice_aggregate_with_null,query_aggregate_date,query_aggregate_buckets.See below for a complete example.The response will be a list of dictionaries with the same attributes as theStatSetinstance.Please notice that the declarative api will create a dictionary and actually call theget_statsfunction so in both cases the result will be exactly the same.Example declarativePlease remember in the example below that if you don’t pass thefieldparameter then the name of the
attribute will be used. Also by default themethodiscount.fromsimple_statsimportfromsimple_statsimport(StatSet,QueryAggregateStat,QueryAggregateSingleStat,ChoiceAggregateStat,QueryAggregateDateStat,QueryAggregateBucketsStat,)classMyStats(StatSet):id=QueryAggregateSingleStat(label='Total number')price=QueryAggregateSingleStat(label='Total price',method='sum')pilot_authority__name=QueryAggregateStat(label='Per authority')pilot_authority__name=QueryAggregateStat(label='Per authority by price',aggr_field='price')status=ChoiceAggregateStat(label='Per status',choices=MyModel.STATUS_CHOICES)status_price=ChoiceAggregateStat(label='Per status by price',choices=MyModel.STATUS_CHOICES,field='status',aggr_field='price'formatter=lambdav:"â‚¬{}".format(v)ifvelse'-')year=QueryAggregateDateStat(label='Per year',what='year',field='created_on')year_price=QueryAggregateDateStat(label='Per year by price',what='year',aggr_field='price',field='created_on')buckets=QueryAggregateBucketsStat(label='Buckets',buckets=[100,50,10])defmy_view(request):qs=TestModel.objects.all()stats=MyStats(qs)returnrender(request,'my_template.html',{'stats':stats})thestatsresult will be an enumerable similar to this one:[{'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 this (using bootstrap):<divclass='row'>{% for s in stats %}<divclass='col-md-4 mb-5'style='max-height: 500px; overflow: auto;'><h4>{{ s.label }}</h4>{% if s.values %}<tableclass='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>Example functionalfromsimple_statsimportget_statsSTATS_CFG=cfg=[{'kind':'query_aggregate_single','label':'Total','field':'id',},{'kind':'query_aggregate_single','label':'Total price','method':'sum','field':'price',},{'kind':'query_aggregate','label':'Per authority','field':'pilot_authority__name',},{'kind':'query_aggregate','label':'Per authority by price','field':'pilot_authority__name','aggr_field':'price','formatter':lambdav:"â‚¬{}".format(v)ifvelse'-'},{'kind':'choice_aggregate','label':'Per status','field':'status','choices':models.STATUS_CHOICES,},{'kind':'choice_aggregate','label':'Per status by price','field':'status','aggr_field':'price','choices':models.STATUS_CHOICES,},{'kind':'query_aggregate_date','label':'Per year','field':'created_on','what':'year',},{'kind':'query_aggregate_date','label':'Per year by price','field':'created_on','what':'year','aggr_field':'price',},{'kind':'query_aggregate_buckets','label':'Per price','field':'price','buckets':[100_00,50_00,1_000,500,0]}]defmy_view(request):qs=TestModel.objects.all()stats=get_stats(qs,STATS_CFG)returnrender(request,'my_template.html',{'stats':stats})Thestatswill be an array of dictionaries, similar to the declarative example.Exporting the statsYou can easily export these stats in xls using the xlwt (https://pypi.org/project/xlwt/) library and this function:importxlwtdefcreate_xls_resp(stats,response):context=self.get_context_data()importxlwtwb=xlwt.Workbook(encoding="utf-8")forstatinstats:ws=wb.add_sheet(stat["label"][:31])ws.write(0,0,stat["label"],xlwt.easyxf('font: name Calibri, bold on',))ifstat["value"]:ws.write(0,1,stat["value"],xlwt.easyxf('font: name Calibri, bold on',))fori,valinenumerate(stat["values"],start=2):forj,vinenumerate(val,start=0):ws.write(i,j,v)wb.save(response)Now you can call it like this from your view:fromdjango.httpimportHttpResponsedefmy_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)returnresponseChangelogv.0.7.0: Addquery_aggregate_extract_dateandQueryAggregateExtractDateStatv.0.6.0: Add tests!v.0.5.1: Allow adding a formatter for the valuesv.0.5.0: Add declarative APIv.0.4.0: Allow the aggregate function to run on a different field usingaggr_fieldv.0.3.1: Fix small bug withchoice_aggregate_with_nullv.0.3.0: Addchoice_aggregate_with_nulland throw if stat kind is not foundv.0.2.1: Fix small bug with column aliasesv.0.2.0: Changed API; usequery_aggregate_datetimefor a datetime field andquery_aggregate_datefor a date fieldv.0.1.0: Initial version