diff --git a/kfet/statistic.py b/kfet/statistic.py index 09f9e935..5b5e297a 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -1,70 +1,117 @@ # -*- coding: utf-8 -*- + +from dateutil.relativedelta import relativedelta + from django.utils import timezone from django.db.models import Sum KFET_WAKES_UP_AT = 7 -# donne le nom des jours d'une liste de dates -# dans un dico ordonné -def daynames(dates): - names = {} - for i in dates: - names[i] = dates[i].strftime("%A") - return names + +def kfet_day(year, month, day, start_at=KFET_WAKES_UP_AT): + return timezone.datetime(year, month, day, hour=start_at) -# donne le nom des semaines une liste de dates -# dans un dico ordonné -def weeknames(dates): - names = {} - for i in dates: - names[i] = dates[i].strftime("Semaine %W") - return names +def to_kfet_day(dt, start_at=KFET_WAKES_UP_AT): + kfet_dt = kfet_day(year=dt.year, month=dt.month, day=dt.day) + if dt.hour < start_at: + kfet_dt -= timezone.timedelta(days=1) + return kfet_dt -# donne le nom des mois d'une liste de dates -# dans un dico ordonné -def monthnames(dates): - names = {} - for i in dates: - names[i] = dates[i].strftime("%B") - return names +class StatScale(object): + name = None + step = None + + def __init__(self, n_steps=0, begin=None, end=None, + last=False, std_chunk=True): + 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 -# dans l'ordre chronologique -# aujourd'hui compris -# nb = 1 : rend hier -def lastdays(nb): - morning = this_morning() - days = {} - for i in range(1, nb+1): - days[i] = morning - timezone.timedelta(days=nb - i + 1) - return days +class DayStatScale(StatScale): + name = 'day' + step = timezone.timedelta(days=1) + label_fmt = '%A' + + @classmethod + def get_chunk_start(cls, dt): + return to_kfet_day(dt) -def lastweeks(nb): - monday_morning = this_monday_morning() - mondays = {} - for i in range(1, nb+1): - mondays[i] = monday_morning \ - - timezone.timedelta(days=7*(nb - i + 1)) - return mondays +class WeekStatScale(StatScale): + name = 'week' + step = timezone.timedelta(days=7) + label_fmt = 'Semaine %W' + + @classmethod + def offset_to_chunk_start(cls, dt): + return timezone.timedelta(days=dt.weekday()) -def lastmonths(nb): - first_month_day = this_first_month_day() - first_days = {} - this_year = first_month_day.year - this_month = first_month_day.month - for i in range(1, nb+1): - month = ((this_month - 1 - (nb - i)) % 12) + 1 - year = this_year + (nb - i) // 12 - first_days[i] = timezone.datetime(year=year, - month=month, - day=1, - hour=KFET_WAKES_UP_AT) - return first_days +class MonthStatScale(StatScale): + name = 'month' + step = relativedelta(months=1) + label_fmt = '%B' + + @classmethod + def get_chunk_start(cls, dt): + return to_kfet_day(dt).replace(day=1) def this_first_month_day(): diff --git a/kfet/urls.py b/kfet/urls.py index ddd40ce6..2eeef513 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -69,18 +69,12 @@ urlpatterns = [ name='kfet.account.negative'), # Account - Statistics - url('^accounts/(?P.{3})/stat/last/$', - views.AccountStatLastAll.as_view(), - name = 'kfet.account.stat.last'), - url('^accounts/(?P.{3})/stat/last/month/$', - views.AccountStatLastMonth.as_view(), - name = 'kfet.account.stat.last.month'), - url('^accounts/(?P.{3})/stat/last/week/$', - views.AccountStatLastWeek.as_view(), - name = 'kfet.account.stat.last.week'), - url('^accounts/(?P.{3})/stat/last/day/$', - views.AccountStatLastDay.as_view(), - name = 'kfet.account.stat.last.day'), + url('^accounts/(?P.{3})/stat/operations/list$', + views.AccountStatOperationList.as_view(), + name='kfet.account.stat.operation.list'), + url('^accounts/(?P.{3})/stat/operations$', + views.AccountStatOperation.as_view(), + name='kfet.account.stat.operation'), url(r'^accounts/(?P.{3})/stat/balance/list$', views.AccountStatBalanceList.as_view(), diff --git a/kfet/views.py b/kfet/views.py index 58413d80..0b62cd2e 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +import ast from urllib.parse import urlencode from django.shortcuts import render, get_object_or_404, redirect @@ -47,10 +48,10 @@ from decimal import Decimal import django_cas_ng import heapq import statistics -from .statistic import daynames, monthnames, weeknames, \ - lastdays, lastweeks, lastmonths, \ - this_morning, this_monday_morning, this_first_month_day, \ - tot_ventes +from kfet.statistic import DayStatScale, MonthStatScale, StatScale, WeekStatScale +from .statistic import ( + this_morning, this_monday_morning, this_first_month_day, tot_ventes, +) class Home(TemplateView): 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 # NE REND PAS DE JSON -class AccountStatLastAll(SingleResumeStat): +class AccountStatOperationList(PkUrlMixin, SingleResumeStat): model = Account context_object_name = 'account' - trigramme_url_kwarg = 'trigramme' + pk_url_kwarg = 'trigramme' id_prefix = ID_PREFIX_ACC_LAST - nb_stat = 3 nb_default = 2 - stat_labels = ["Derniers mois", "Dernières semaines", "Derniers jours"] - stat_urls = ['kfet.account.stat.last.month', - 'kfet.account.stat.last.week', - 'kfet.account.stat.last.day'] + stats = [ + { + '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' - def get_object(self, **kwargs): - trigramme = self.kwargs.get(self.trigramme_url_kwarg) - return get_object_or_404(Account, trigramme=trigramme) - - def get_object_url_kwargs(self, **kwargs): - return {'trigramme': self.object.trigramme} + 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) 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 consommation of a trigramme at the diffent dates specified """ model = Account - trigramme_url_kwarg = 'trigramme' + pk_url_kwarg = 'trigramme' context_object_name = 'account' - end_date = timezone.now() id_prefix = "" - # 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): - 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 + def get_operations(self, types, scale): # On selectionne les opérations qui correspondent # à l'article en question et qui ne sont pas annulées # puis on choisi pour chaques intervalle les opérations # effectuées dans ces intervalles de temps all_operations = (Operation.objects - .filter(type='purchase') + .filter(type__in=types) .filter(group__on_acc=self.object) .filter(canceled_at=None) ) - operations = {} - for i in dates: - operations[i] = (all_operations - .filter(group__at__gte=extended_dates[i]) - .filter(group__at__lte=extended_dates[i+1]) - ) + operations = [] + for begin, end in scale: + operations.append(all_operations + .filter(group__at__gte=begin, + group__at__lte=end)) return operations def get_context_data(self, **kwargs): 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 - context['labels'] = self.get_labels().copy() + context['labels'] = scale.get_labels() # On compte les opérations - operations = self.sort_operations() - for i in operations: - nb_ventes[i] = tot_ventes(operations[i]) - context['charts'] = [ { "color": "rgb(255, 99, 132)", - "label": "NB items achetés", - "values": nb_ventes } ] + nb_ventes = [] + for chunk in operations: + nb_ventes.append(tot_ventes(chunk)) + + context['labels'] = scale.get_labels() + context['charts'] = [{"color": "rgb(255, 99, 132)", + "label": "NB items achetés", + "values": nb_ventes}] 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) def dispatch(self, *args, **kwargs): - return super(AccountStatLast, self).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) + return super().dispatch(*args, **kwargs) # ------------------------