Clean Article stats

kfet.statistic
- delete no longer used defs
- new mixin - ScaleMixin
  - get scale args from GET params
  - chunkify querysets according to a scale

Article stats
- use SingleResumeStat for manifest
- use ScaleMixin for sales
- update urls
- update permission required: teamkfet

Account stats
- update permission required: teamkfet
- operations use ScaleMixin
- fix manifests urls
This commit is contained in:
Aurélien Delobelle 2017-04-03 03:12:52 +02:00
parent f585247224
commit c01de558e1
5 changed files with 132 additions and 221 deletions

View file

@ -1,5 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import ast
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.utils import timezone from django.utils import timezone
@ -50,7 +52,7 @@ class StatScale(object):
for cls in StatScale.__subclasses__(): for cls in StatScale.__subclasses__():
if cls.name == name: if cls.name == name:
return cls return cls
raise Exception('scale %s not found' % name) return None
def get_from(self, dt): def get_from(self, dt):
return self.std_chunk and self.get_chunk_start(dt) or dt return self.std_chunk and self.get_chunk_start(dt) or dt
@ -114,32 +116,38 @@ class MonthStatScale(StatScale):
return to_kfet_day(dt).replace(day=1) return to_kfet_day(dt).replace(day=1)
def this_first_month_day(): def stat_manifest(scales_def=None, scale_args=None, **url_params):
now = timezone.now() if scales_def is None:
first_day = timezone.datetime(year=now.year, scales_def = []
month=now.month, if scale_args is None:
day=1, scale_args = {}
hour=KFET_WAKES_UP_AT) return [
return first_day dict(
label=label,
url_params=dict(
scale=cls.name,
scale_args=scale_args,
**url_params,
),
)
for label, cls in scales_def
]
def this_monday_morning(): def last_stats_manifest(scales_def=None, scale_args=None, **url_params):
now = timezone.now() scales_def = [
monday = now - timezone.timedelta(days=now.isoweekday()-1) ('Derniers mois', MonthStatScale, ),
monday_morning = timezone.datetime(year=monday.year, ('Dernières semaines', WeekStatScale, ),
month=monday.month, ('Derniers jours', DayStatScale, ),
day=monday.day, ]
hour=KFET_WAKES_UP_AT) if scale_args is None:
return monday_morning scale_args = {}
scale_args.update(dict(
last=True,
def this_morning(): n_steps=7,
now = timezone.now() ))
morning = timezone.datetime(year=now.year, return stat_manifest(scales_def=scales_def, scale_args=scale_args,
month=now.month, **url_params)
day=now.day,
hour=KFET_WAKES_UP_AT)
return morning
# Étant donné un queryset d'operations # Étant donné un queryset d'operations
@ -147,3 +155,38 @@ def this_morning():
def tot_ventes(queryset): def tot_ventes(queryset):
res = queryset.aggregate(Sum('article_nb'))['article_nb__sum'] res = queryset.aggregate(Sum('article_nb'))['article_nb__sum']
return res and res or 0 return res and res or 0
class ScaleMixin(object):
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
scale_name = self.request.GET.get('scale', None)
scale_args = self.request.GET.get('scale_args', None)
cls = StatScale.by_name(scale_name)
if cls is None:
scale = self.get_default_scale()
else:
scale_args = self.request.GET.get('scale_args', {})
if isinstance(scale_args, str):
scale_args = ast.literal_eval(scale_args)
scale = cls(**scale_args)
self.scale = scale
context['labels'] = scale.get_labels()
return context
def get_default_scale(self):
return DayStatScale(n_steps=7, last=True)
def chunkify_qs(self, qs, scale, field=None):
if field is None:
field = 'at'
begin_f = '{}__gte'.format(field)
end_f = '{}__lte'.format(field)
return [
qs.filter(**{begin_f: begin, end_f: end})
for begin, end in scale
]

View file

@ -17,11 +17,11 @@
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
var stat_last = new StatsGroup( var stat_last = new StatsGroup(
"{% url 'kfet.account.stat.last' trigramme=account.trigramme %}", "{% url 'kfet.account.stat.operation.list' trigramme=account.trigramme %}",
$("#stat_last"), $("#stat_last"),
); );
var stat_balance = new StatsGroup( var stat_balance = new StatsGroup(
"{% url 'kfet.account.stat.balance' trigramme=account.trigramme %}", "{% url 'kfet.account.stat.balance.list' trigramme=account.trigramme %}",
$("#stat_balance"), $("#stat_balance"),
); );
}); });

View file

@ -99,13 +99,16 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %}
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
console.log('echo');
var stat_last = new StatsGroup( var stat_last = new StatsGroup(
"{% url 'kfet.article.stat.last' article.id %}", "{% url 'kfet.article.stat.sales.list' article.id %}",
$("#stat_last"), $("#stat_last"),
); );
console.log('echo');
}); });
</script> </script>
{% endblock %}

View file

@ -140,20 +140,14 @@ urlpatterns = [
# Article - Update # Article - Update
url('^articles/(?P<pk>\d+)/edit$', url('^articles/(?P<pk>\d+)/edit$',
teamkfet_required(views.ArticleUpdate.as_view()), teamkfet_required(views.ArticleUpdate.as_view()),
name = 'kfet.article.update'), name='kfet.article.update'),
# Article - Statistics # Article - Statistics
url('^articles/(?P<pk>\d+)/stat/last/$', url(r'^articles/(?P<pk>\d+)/stat/sales/list$',
views.ArticleStatLastAll.as_view(), views.ArticleStatSalesList.as_view(),
name = 'kfet.article.stat.last'), name='kfet.article.stat.sales.list'),
url('^articles/(?P<pk>\d+)/stat/last/month/$', url(r'^articles/(?P<pk>\d+)/stat/sales$',
views.ArticleStatLastMonth.as_view(), views.ArticleStatSales.as_view(),
name = 'kfet.article.stat.last.month'), name='kfet.article.stat.sales'),
url('^articles/(?P<pk>\d+)/stat/last/week/$',
views.ArticleStatLastWeek.as_view(),
name = 'kfet.article.stat.last.week'),
url('^articles/(?P<pk>\d+)/stat/last/day/$',
views.ArticleStatLastDay.as_view(),
name = 'kfet.article.stat.last.day'),
# ----- # -----
# K-Psul urls # K-Psul urls

View file

@ -48,10 +48,8 @@ from decimal import Decimal
import django_cas_ng import django_cas_ng
import heapq import heapq
import statistics import statistics
from kfet.statistic import DayStatScale, MonthStatScale, StatScale, WeekStatScale from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes
from .statistic import (
this_morning, this_monday_morning, this_first_month_day, tot_ventes,
)
class Home(TemplateView): class Home(TemplateView):
template_name = "kfet/home.html" template_name = "kfet/home.html"
@ -2155,10 +2153,8 @@ class AccountStatBalance(PkUrlMixin, JSONDetailView):
account = self.object account = self.object
# prepare filters # prepare filters
if end_date is None:
end_date = this_morning()
if last_days is not None: if last_days is not None:
end_date = timezone.now()
begin_date = end_date - timezone.timedelta(days=last_days) begin_date = end_date - timezone.timedelta(days=last_days)
# prepare querysets # prepare querysets
@ -2289,43 +2285,7 @@ class AccountStatOperationList(PkUrlMixin, SingleResumeStat):
pk_url_kwarg = 'trigramme' pk_url_kwarg = 'trigramme'
id_prefix = ID_PREFIX_ACC_LAST id_prefix = ID_PREFIX_ACC_LAST
nb_default = 2 nb_default = 2
stats = [ stats = last_stats_manifest(types=[Operation.PURCHASE])
{
'label': 'Derniers mois',
'url_params': dict(
types=[Operation.PURCHASE],
scale=MonthStatScale.name,
scale_args=dict(
last=True,
n_steps=7,
),
),
},
{
'label': 'Dernières semaines',
'url_params': dict(
types=[Operation.PURCHASE],
last_days=49,
scale=WeekStatScale.name,
scale_args=dict(
last=True,
n_steps=7,
),
),
},
{
'label': 'Derniers jours',
'url_params': dict(
types=[Operation.PURCHASE],
last_days=7,
scale=DayStatScale.name,
scale_args=dict(
last=True,
n_steps=7,
),
),
},
]
url_stat = 'kfet.account.stat.operation' url_stat = 'kfet.account.stat.operation'
def get_object(self, *args, **kwargs): def get_object(self, *args, **kwargs):
@ -2339,7 +2299,7 @@ class AccountStatOperationList(PkUrlMixin, SingleResumeStat):
return super().dispatch(*args, **kwargs) return super().dispatch(*args, **kwargs)
class AccountStatOperation(PkUrlMixin, JSONDetailView): class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView):
""" """
Returns a JSON containing the evolution a the personnal Returns a JSON containing the evolution a the personnal
consommation of a trigramme at the diffent dates specified consommation of a trigramme at the diffent dates specified
@ -2359,38 +2319,24 @@ class AccountStatOperation(PkUrlMixin, JSONDetailView):
.filter(group__on_acc=self.object) .filter(group__on_acc=self.object)
.filter(canceled_at=None) .filter(canceled_at=None)
) )
operations = [] chunks = self.chunkify_qs(all_operations, scale, field='group__at')
for begin, end in scale: return chunks
operations.append(all_operations
.filter(group__at__gte=begin,
group__at__lte=end))
return operations
def get_context_data(self, **kwargs): def get_context_data(self, *args, **kwargs):
context = {} old_ctx = super().get_context_data(*args, **kwargs)
context = {'labels': old_ctx['labels']}
scale = self.request.GET.get('scale', None) scale = self.scale
if scale is None:
scale = DayStatScale(n_steps=7, last=True)
else:
scale_cls = StatScale.by_name(scale)
scale_args = self.request.GET.get('scale_args', '{}')
scale = scale_cls(**ast.literal_eval(scale_args))
print(scale.datetimes)
types = self.request.GET.get('types', None) types = self.request.GET.get('types', None)
if types is not None: if types is not None:
types = ast.literal_eval(types) types = ast.literal_eval(types)
operations = self.get_operations(types=types, scale=scale) operations = self.get_operations(types=types, scale=scale)
# On récupère les labels des dates
context['labels'] = scale.get_labels()
# On compte les opérations # On compte les opérations
nb_ventes = [] nb_ventes = []
for chunk in operations: for chunk in operations:
nb_ventes.append(tot_ventes(chunk)) nb_ventes.append(tot_ventes(chunk))
context['labels'] = scale.get_labels()
context['charts'] = [{"color": "rgb(255, 99, 132)", context['charts'] = [{"color": "rgb(255, 99, 132)",
"label": "NB items achetés", "label": "NB items achetés",
"values": nb_ventes}] "values": nb_ventes}]
@ -2418,141 +2364,66 @@ ID_PREFIX_ART_LAST_MONTHS = "last_months_art"
# Un résumé de toutes les vues ArticleStatLast # Un résumé de toutes les vues ArticleStatLast
# NE REND PAS DE JSON # NE REND PAS DE JSON
class ArticleStatLastAll(SingleResumeStat): class ArticleStatSalesList(SingleResumeStat):
model = Article model = Article
context_object_name = 'article' context_object_name = 'article'
id_prefix = ID_PREFIX_ART_LAST id_prefix = ID_PREFIX_ART_LAST
nb_stat = 3
nb_default = 2 nb_default = 2
stat_labels = ["Derniers mois", "Dernières semaines", "Derniers jours"] url_stat = 'kfet.article.stat.sales'
stat_urls = ['kfet.article.stat.last.month', stats = last_stats_manifest()
'kfet.article.stat.last.week',
'kfet.article.stat.last.day']
@method_decorator(login_required) @method_decorator(teamkfet_required)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
return super(ArticleStatLastAll, self).dispatch(*args, **kwargs) return super().dispatch(*args, **kwargs)
class ArticleStatLast(JSONDetailView): class ArticleStatSales(ScaleMixin, JSONDetailView):
""" """
Returns a JSON containing the consommation Returns a JSON containing the consommation
of an article at the diffent dates precised of an article at the diffent dates precised
""" """
model = Article model = Article
context_object_name = 'article' context_object_name = 'article'
end_date = timezone.now()
id_prefix = ""
def render_to_response(self, context): def get_context_data(self, *args, **kwargs):
# Look for a 'format=json' GET argument old_ctx = super().get_context_data(*args, **kwargs)
if self.request.GET.get('format') == 'json': context = {'labels': old_ctx['labels']}
return self.render_to_json_response(context) scale = self.scale
else:
return super(ArticleStatLast, self).render_to_response(context)
# doit rendre un dictionnaire des dates
# la première date correspond au début
# la dernière date est la fin de la dernière plage
def get_dates(self, **kwargs):
pass
# doit rendre un dictionnaire des labels
# le dernier label ne sera pas utilisé
def get_labels(self, **kwargs):
pass
def get_context_data(self, **kwargs):
context = {}
# On récupère les labels des dates
context['labels'] = self.get_labels().copy()
# On récupère les dates
dates = self.get_dates()
# On ajoute la date de fin
extended_dates = dates.copy()
extended_dates[len(dates)+1] = self.end_date
# On selectionne les opérations qui correspondent # On selectionne les opérations qui correspondent
# à l'article en question et qui ne sont pas annulées # à l'article en question et qui ne sont pas annulées
# puis on choisi pour chaques intervalle les opérations # puis on choisi pour chaques intervalle les opérations
# effectuées dans ces intervalles de temps # effectuées dans ces intervalles de temps
all_operations = (Operation.objects all_operations = (
.filter(type='purchase') Operation.objects
.filter(article=self.object) .filter(type=Operation.PURCHASE,
.filter(canceled_at=None) article=self.object,
) canceled_at=None,
operations = {} )
for i in dates: )
operations[i] = (all_operations chunks = self.chunkify_qs(all_operations, scale, field='group__at')
.filter(group__at__gte=extended_dates[i])
.filter(group__at__lte=extended_dates[i+1])
)
# On compte les opérations # On compte les opérations
nb_ventes = {} nb_ventes = []
nb_accounts = {} nb_accounts = []
nb_liq = {} nb_liq = []
for i in operations: for qs in chunks:
nb_ventes[i] = tot_ventes(operations[i]) nb_ventes.append(
nb_liq[i] = tot_ventes( tot_ventes(qs))
operations[i] nb_liq.append(
.filter(group__on_acc__trigramme='LIQ') tot_ventes(qs.filter(group__on_acc__trigramme='LIQ')))
) nb_accounts.append(
nb_accounts[i] = tot_ventes( tot_ventes(qs.exclude(group__on_acc__trigramme='LIQ')))
operations[i] context['charts'] = [{"color": "rgb(255, 99, 132)",
.exclude(group__on_acc__trigramme='LIQ') "label": "Toutes consommations",
) "values": nb_ventes},
context['charts'] = [ { "color": "rgb(255, 99, 132)", {"color": "rgb(54, 162, 235)",
"label": "Toutes consommations", "label": "LIQ",
"values": nb_ventes }, "values": nb_liq},
{ "color": "rgb(54, 162, 235)", {"color": "rgb(255, 205, 86)",
"label": "LIQ", "label": "Comptes K-Fêt",
"values": nb_liq }, "values": nb_accounts}]
{ "color": "rgb(255, 205, 86)",
"label": "Comptes K-Fêt",
"values": nb_accounts } ]
return context return context
@method_decorator(login_required) @method_decorator(teamkfet_required)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
return super(ArticleStatLast, self).dispatch(*args, **kwargs) return super().dispatch(*args, **kwargs)
# Rend les ventes des 7 derniers jours
# Aujourd'hui non compris
class ArticleStatLastDay(ArticleStatLast):
end_date = this_morning()
id_prefix = ID_PREFIX_ART_LAST_DAYS
def get_dates(self, **kwargs):
return lastdays(7)
def get_labels(self, **kwargs):
days = lastdays(7)
return daynames(days)
# Rend les ventes de 7 dernières semaines
# La semaine en cours n'est pas comprise
class ArticleStatLastWeek(ArticleStatLast):
end_date = this_monday_morning()
id_prefix = ID_PREFIX_ART_LAST_WEEKS
def get_dates(self, **kwargs):
return lastweeks(7)
def get_labels(self, **kwargs):
weeks = lastweeks(7)
return weeknames(weeks)
# Rend les ventes des 7 derniers mois
# Le mois en cours n'est pas compris
class ArticleStatLastMonth(ArticleStatLast):
end_date = this_monday_morning()
id_prefix = ID_PREFIX_ART_LAST_MONTHS
def get_dates(self, **kwargs):
return lastmonths(7)
def get_labels(self, **kwargs):
months = lastmonths(7)
return monthnames(months)