- get_scale_args method of ScaleMixin retrieves useful GET params for Scale object instanciation (by default from request.GET) - it takes into account the type of the scale arg - prefix used for GET param can be modified in stats_manifest funcs and ScaleMixin
235 lines
6.6 KiB
Python
235 lines
6.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
from datetime import date, datetime, time, timedelta
|
|
|
|
from dateutil.relativedelta import relativedelta
|
|
from dateutil.parser import parse as dateutil_parse
|
|
|
|
from django.utils import timezone
|
|
from django.db.models import Sum
|
|
|
|
KFET_WAKES_UP_AT = time(7, 0)
|
|
|
|
|
|
def kfet_day(year, month, day, start_at=KFET_WAKES_UP_AT):
|
|
"""datetime wrapper with time offset."""
|
|
return datetime.combine(date(year, month, day), start_at)
|
|
|
|
|
|
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.time() < start_at:
|
|
kfet_dt -= timedelta(days=1)
|
|
return kfet_dt
|
|
|
|
|
|
class Scale(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 Scale.__subclasses__():
|
|
if cls.name == name:
|
|
return cls
|
|
return None
|
|
|
|
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]
|
|
|
|
|
|
class DayScale(Scale):
|
|
name = 'day'
|
|
step = timedelta(days=1)
|
|
label_fmt = '%A'
|
|
|
|
@classmethod
|
|
def get_chunk_start(cls, dt):
|
|
return to_kfet_day(dt)
|
|
|
|
|
|
class WeekScale(Scale):
|
|
name = 'week'
|
|
step = timedelta(days=7)
|
|
label_fmt = 'Semaine %W'
|
|
|
|
@classmethod
|
|
def get_chunk_start(cls, dt):
|
|
dt_kfet = to_kfet_day(dt)
|
|
offset = timedelta(days=dt_kfet.weekday())
|
|
return dt_kfet - offset
|
|
|
|
|
|
class MonthScale(Scale):
|
|
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 stat_manifest(scales_def=None, scale_args=None, scale_prefix=None,
|
|
**other_url_params):
|
|
if scale_prefix is None:
|
|
scale_prefix = 'scale_'
|
|
if scales_def is None:
|
|
scales_def = []
|
|
if scale_args is None:
|
|
scale_args = {}
|
|
manifest = []
|
|
for label, cls in scales_def:
|
|
url_params = {scale_prefix+'name': cls.name}
|
|
url_params.update({scale_prefix+key: value
|
|
for key, value in scale_args.items()})
|
|
url_params.update(other_url_params)
|
|
manifest.append(dict(
|
|
label=label,
|
|
url_params=url_params,
|
|
))
|
|
return manifest
|
|
|
|
|
|
def last_stats_manifest(scales_def=None, scale_args=None, scale_prefix=None,
|
|
**url_params):
|
|
scales_def = [
|
|
('Derniers mois', MonthScale, ),
|
|
('Dernières semaines', WeekScale, ),
|
|
('Derniers jours', DayScale, ),
|
|
]
|
|
if scale_args is None:
|
|
scale_args = {}
|
|
scale_args.update(dict(
|
|
last=True,
|
|
n_steps=7,
|
|
))
|
|
return stat_manifest(scales_def=scales_def, scale_args=scale_args,
|
|
scale_prefix=scale_prefix, **url_params)
|
|
|
|
|
|
# Étant donné un queryset d'operations
|
|
# rend la somme des article_nb
|
|
def tot_ventes(queryset):
|
|
res = queryset.aggregate(Sum('article_nb'))['article_nb__sum']
|
|
return res and res or 0
|
|
|
|
|
|
class ScaleMixin(object):
|
|
scale_args_prefix = 'scale_'
|
|
|
|
def get_scale_args(self, params=None, prefix=None):
|
|
"""Retrieve scale args from params.
|
|
|
|
Should search the same args of Scale constructor.
|
|
|
|
Args:
|
|
params (dict, optional): Scale args are searched in this.
|
|
Default to GET params of request.
|
|
prefix (str, optional): Appended at the begin of scale args names.
|
|
Default to `self.scale_args_prefix`.
|
|
|
|
"""
|
|
if params is None:
|
|
params = self.request.GET
|
|
if prefix is None:
|
|
prefix = self.scale_args_prefix
|
|
|
|
scale_args = {}
|
|
|
|
name = params.get(prefix+'name', None)
|
|
if name is not None:
|
|
scale_args['name'] = name
|
|
|
|
n_steps = params.get(prefix+'n_steps', None)
|
|
if n_steps is not None:
|
|
scale_args['n_steps'] = int(n_steps)
|
|
|
|
begin = params.get(prefix+'begin', None)
|
|
if begin is not None:
|
|
scale_args['begin'] = dateutil_parse(begin)
|
|
|
|
end = params.get(prefix+'send', None)
|
|
if end is not None:
|
|
scale_args['end'] = dateutil_parse(end)
|
|
|
|
last = params.get(prefix+'last', None)
|
|
if last is not None:
|
|
scale_args['last'] = (
|
|
last in ['true', 'True', '1'] and True or False)
|
|
|
|
return scale_args
|
|
|
|
def get_context_data(self, *args, **kwargs):
|
|
context = super().get_context_data(*args, **kwargs)
|
|
|
|
scale_args = self.get_scale_args()
|
|
scale_name = scale_args.pop('name', None)
|
|
scale_cls = Scale.by_name(scale_name)
|
|
|
|
if scale_cls is None:
|
|
scale = self.get_default_scale()
|
|
else:
|
|
scale = scale_cls(**scale_args)
|
|
|
|
self.scale = scale
|
|
context['labels'] = scale.get_labels()
|
|
return context
|
|
|
|
def get_default_scale(self):
|
|
return DayScale(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
|
|
]
|