Refactor Account Operations stats

K-Fêt - Statistics

New base class - StatScale
- create scale, given chunk size (timedelta), start and end times
- get labels of
- get start and end datetimes of chunks

DayStatScale: Scale whose chunks interval is 1 day
WeekStatScale: same with 1 week
MonthStatScale: same with 1 month

AccountStatOperationList: manifest of operations stats of an account
- renamed from AccountStatLastAll
- updated according to SingleResumeStat

AccountStatOperation:
- renamed from AccountStatLast
- remove scale logic with use of StatScale objects
- used scale is given by `scale` and `scale_args` GET params
- add filter on operations types with `types` GET param

AccountStatLast(Day,Week,Month) are deleted ("merged" in
AccountStatOperation)
This commit is contained in:
Aurélien Delobelle 2017-04-03 00:40:52 +02:00
parent 31261fd376
commit f585247224
3 changed files with 195 additions and 163 deletions

View file

@ -1,70 +1,117 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from dateutil.relativedelta import relativedelta
from django.utils import timezone from django.utils import timezone
from django.db.models import Sum from django.db.models import Sum
KFET_WAKES_UP_AT = 7 KFET_WAKES_UP_AT = 7
# donne le nom des jours d'une liste de dates
# dans un dico ordonné def kfet_day(year, month, day, start_at=KFET_WAKES_UP_AT):
def daynames(dates): return timezone.datetime(year, month, day, hour=start_at)
names = {}
for i in dates:
names[i] = dates[i].strftime("%A")
return names
# donne le nom des semaines une liste de dates def to_kfet_day(dt, start_at=KFET_WAKES_UP_AT):
# dans un dico ordonné kfet_dt = kfet_day(year=dt.year, month=dt.month, day=dt.day)
def weeknames(dates): if dt.hour < start_at:
names = {} kfet_dt -= timezone.timedelta(days=1)
for i in dates: return kfet_dt
names[i] = dates[i].strftime("Semaine %W")
return names
# donne le nom des mois d'une liste de dates class StatScale(object):
# dans un dico ordonné name = None
def monthnames(dates): step = None
names = {}
for i in dates: def __init__(self, n_steps=0, begin=None, end=None,
names[i] = dates[i].strftime("%B") last=False, std_chunk=True):
return names self.std_chunk = std_chunk
if last:
end = timezone.now()
if begin is not None and n_steps != 0:
self.begin = self.get_from(begin)
self.end = self.do_step(self.begin, n_steps=n_steps)
elif end is not None and n_steps != 0:
self.end = self.get_from(end)
self.begin = self.do_step(self.end, n_steps=-n_steps)
elif begin is not None and end is not None:
self.begin = self.get_from(begin)
self.end = self.get_from(end)
else:
raise Exception('Two of these args must be specified: '
'n_steps, begin, end; '
'or use last and n_steps')
self.datetimes = self.get_datetimes()
@staticmethod
def by_name(name):
for cls in StatScale.__subclasses__():
if cls.name == name:
return cls
raise Exception('scale %s not found' % name)
def get_from(self, dt):
return self.std_chunk and self.get_chunk_start(dt) or dt
def __getitem__(self, i):
return self.datetimes[i], self.datetimes[i+1]
def __len__(self):
return len(self.datetimes) - 1
def do_step(self, dt, n_steps=1):
return dt + self.step * n_steps
def get_datetimes(self):
datetimes = [self.begin]
tmp = self.begin
while tmp <= self.end:
tmp = self.do_step(tmp)
datetimes.append(tmp)
return datetimes
def get_labels(self, label_fmt=None):
if label_fmt is None:
label_fmt = self.label_fmt
return [begin.strftime(label_fmt) for begin, end in self]
@classmethod
def get_chunk_start(cls, dt):
dt_kfet = to_kfet_day(dt)
start = dt_kfet - cls.offset_to_chunk_start(dt_kfet)
return start
# rend les dates des nb derniers jours class DayStatScale(StatScale):
# dans l'ordre chronologique name = 'day'
# aujourd'hui compris step = timezone.timedelta(days=1)
# nb = 1 : rend hier label_fmt = '%A'
def lastdays(nb):
morning = this_morning() @classmethod
days = {} def get_chunk_start(cls, dt):
for i in range(1, nb+1): return to_kfet_day(dt)
days[i] = morning - timezone.timedelta(days=nb - i + 1)
return days
def lastweeks(nb): class WeekStatScale(StatScale):
monday_morning = this_monday_morning() name = 'week'
mondays = {} step = timezone.timedelta(days=7)
for i in range(1, nb+1): label_fmt = 'Semaine %W'
mondays[i] = monday_morning \
- timezone.timedelta(days=7*(nb - i + 1)) @classmethod
return mondays def offset_to_chunk_start(cls, dt):
return timezone.timedelta(days=dt.weekday())
def lastmonths(nb): class MonthStatScale(StatScale):
first_month_day = this_first_month_day() name = 'month'
first_days = {} step = relativedelta(months=1)
this_year = first_month_day.year label_fmt = '%B'
this_month = first_month_day.month
for i in range(1, nb+1): @classmethod
month = ((this_month - 1 - (nb - i)) % 12) + 1 def get_chunk_start(cls, dt):
year = this_year + (nb - i) // 12 return to_kfet_day(dt).replace(day=1)
first_days[i] = timezone.datetime(year=year,
month=month,
day=1,
hour=KFET_WAKES_UP_AT)
return first_days
def this_first_month_day(): def this_first_month_day():

View file

@ -69,18 +69,12 @@ urlpatterns = [
name='kfet.account.negative'), name='kfet.account.negative'),
# Account - Statistics # Account - Statistics
url('^accounts/(?P<trigramme>.{3})/stat/last/$', url('^accounts/(?P<trigramme>.{3})/stat/operations/list$',
views.AccountStatLastAll.as_view(), views.AccountStatOperationList.as_view(),
name = 'kfet.account.stat.last'), name='kfet.account.stat.operation.list'),
url('^accounts/(?P<trigramme>.{3})/stat/last/month/$', url('^accounts/(?P<trigramme>.{3})/stat/operations$',
views.AccountStatLastMonth.as_view(), views.AccountStatOperation.as_view(),
name = 'kfet.account.stat.last.month'), name='kfet.account.stat.operation'),
url('^accounts/(?P<trigramme>.{3})/stat/last/week/$',
views.AccountStatLastWeek.as_view(),
name = 'kfet.account.stat.last.week'),
url('^accounts/(?P<trigramme>.{3})/stat/last/day/$',
views.AccountStatLastDay.as_view(),
name = 'kfet.account.stat.last.day'),
url(r'^accounts/(?P<trigramme>.{3})/stat/balance/list$', url(r'^accounts/(?P<trigramme>.{3})/stat/balance/list$',
views.AccountStatBalanceList.as_view(), views.AccountStatBalanceList.as_view(),

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import ast
from urllib.parse import urlencode from urllib.parse import urlencode
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
@ -47,10 +48,10 @@ from decimal import Decimal
import django_cas_ng import django_cas_ng
import heapq import heapq
import statistics import statistics
from .statistic import daynames, monthnames, weeknames, \ from kfet.statistic import DayStatScale, MonthStatScale, StatScale, WeekStatScale
lastdays, lastweeks, lastmonths, \ from .statistic import (
this_morning, this_monday_morning, this_first_month_day, \ this_morning, this_monday_morning, this_first_month_day, tot_ventes,
tot_ventes )
class Home(TemplateView): class Home(TemplateView):
template_name = "kfet/home.html" template_name = "kfet/home.html"
@ -2282,138 +2283,128 @@ ID_PREFIX_ACC_LAST_MONTHS = "last_months_acc"
# 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 AccountStatLastAll(SingleResumeStat): class AccountStatOperationList(PkUrlMixin, SingleResumeStat):
model = Account model = Account
context_object_name = 'account' context_object_name = 'account'
trigramme_url_kwarg = 'trigramme' pk_url_kwarg = 'trigramme'
id_prefix = ID_PREFIX_ACC_LAST id_prefix = ID_PREFIX_ACC_LAST
nb_stat = 3
nb_default = 2 nb_default = 2
stat_labels = ["Derniers mois", "Dernières semaines", "Derniers jours"] stats = [
stat_urls = ['kfet.account.stat.last.month', {
'kfet.account.stat.last.week', 'label': 'Derniers mois',
'kfet.account.stat.last.day'] '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'
def get_object(self, **kwargs): def get_object(self, *args, **kwargs):
trigramme = self.kwargs.get(self.trigramme_url_kwarg) obj = super().get_object(*args, **kwargs)
return get_object_or_404(Account, trigramme=trigramme) if self.request.user != obj.user:
raise PermissionDenied
def get_object_url_kwargs(self, **kwargs): return obj
return {'trigramme': self.object.trigramme}
@method_decorator(login_required) @method_decorator(login_required)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
return super(AccountStatLastAll, self).dispatch(*args, **kwargs) return super().dispatch(*args, **kwargs)
class AccountStatLast(JSONDetailView): class AccountStatOperation(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
""" """
model = Account model = Account
trigramme_url_kwarg = 'trigramme' pk_url_kwarg = 'trigramme'
context_object_name = 'account' context_object_name = 'account'
end_date = timezone.now()
id_prefix = "" id_prefix = ""
# doit rendre un dictionnaire des dates def get_operations(self, types, scale):
# 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):
return {}
# doit rendre un dictionnaire des labels
# le dernier label ne sera pas utilisé
def get_labels(self, **kwargs):
pass
def get_object(self, **kwargs):
trigramme = self.kwargs.get(self.trigramme_url_kwarg)
return get_object_or_404(Account, trigramme=trigramme)
def sort_operations(self, **kwargs):
# 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 = (Operation.objects
.filter(type='purchase') .filter(type__in=types)
.filter(group__on_acc=self.object) .filter(group__on_acc=self.object)
.filter(canceled_at=None) .filter(canceled_at=None)
) )
operations = {} operations = []
for i in dates: for begin, end in scale:
operations[i] = (all_operations operations.append(all_operations
.filter(group__at__gte=extended_dates[i]) .filter(group__at__gte=begin,
.filter(group__at__lte=extended_dates[i+1]) group__at__lte=end))
)
return operations return operations
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = {} context = {}
nb_ventes = {}
scale = self.request.GET.get('scale', None)
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)
if types is not None:
types = ast.literal_eval(types)
operations = self.get_operations(types=types, scale=scale)
# On récupère les labels des dates # On récupère les labels des dates
context['labels'] = self.get_labels().copy() context['labels'] = scale.get_labels()
# On compte les opérations # On compte les opérations
operations = self.sort_operations() nb_ventes = []
for i in operations: for chunk in operations:
nb_ventes[i] = tot_ventes(operations[i]) nb_ventes.append(tot_ventes(chunk))
context['charts'] = [ { "color": "rgb(255, 99, 132)",
"label": "NB items achetés", context['labels'] = scale.get_labels()
"values": nb_ventes } ] context['charts'] = [{"color": "rgb(255, 99, 132)",
"label": "NB items achetés",
"values": nb_ventes}]
return context return context
def get_object(self, *args, **kwargs):
obj = super().get_object(*args, **kwargs)
if self.request.user != obj.user:
raise PermissionDenied
return obj
@method_decorator(login_required) @method_decorator(login_required)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
return super(AccountStatLast, self).dispatch(*args, **kwargs) return super().dispatch(*args, **kwargs)
# Rend les achats pour ce compte des 7 derniers jours
# Aujourd'hui non compris
class AccountStatLastDay(AccountStatLast):
end_date = this_morning()
id_prefix = ID_PREFIX_ACC_LAST_DAYS
def get_dates(self, **kwargs):
return lastdays(7)
def get_labels(self, **kwargs):
days = lastdays(7)
return daynames(days)
# Rend les achats de ce compte des 7 dernières semaines
# La semaine en cours n'est pas comprise
class AccountStatLastWeek(AccountStatLast):
end_date = this_monday_morning()
id_prefix = ID_PREFIX_ACC_LAST_WEEKS
def get_dates(self, **kwargs):
return lastweeks(7)
def get_labels(self, **kwargs):
weeks = lastweeks(7)
return weeknames(weeks)
# Rend les achats de ce compte des 7 derniers mois
# Le mois en cours n'est pas compris
class AccountStatLastMonth(AccountStatLast):
end_date = this_monday_morning()
id_prefix = ID_PREFIX_ACC_LAST_MONTHS
def get_dates(self, **kwargs):
return lastmonths(7)
def get_labels(self, **kwargs):
months = lastmonths(7)
return monthnames(months)
# ------------------------ # ------------------------