Refactor py base stats and account balance stats

New mixin: PkUrlMixin
- use with SingleObjectMixin standard django mixin (used by
  DetailView...)
- `get_object` use field declared in `pk_url_kwarg` to get... the object

SingleResumeStat
- clean (part of) py code

AccountStatBalanceList
- renamed from `AccountStatBalanceAll`
- url modified
- add permission checking (only the connected user can get balance
  stats manifest)
- clean py code

AccountStatBalance
- cleaner filtering management
- merge urls using this class
- clean py code
This commit is contained in:
Aurélien Delobelle 2017-04-02 17:03:20 +02:00
parent f6022ecf7d
commit 87b9db520f
2 changed files with 141 additions and 142 deletions

View file

@ -82,15 +82,12 @@ urlpatterns = [
views.AccountStatLastDay.as_view(),
name = 'kfet.account.stat.last.day'),
url('^accounts/(?P<trigramme>.{3})/stat/balance/$',
views.AccountStatBalanceAll.as_view(),
name = 'kfet.account.stat.balance'),
url('^accounts/(?P<trigramme>.{3})/stat/balance/d/(?P<nb_date>\d*)/$',
url(r'^accounts/(?P<trigramme>.{3})/stat/balance/list$',
views.AccountStatBalanceList.as_view(),
name='kfet.account.stat.balance.list'),
url(r'^accounts/(?P<trigramme>.{3})/stat/balance$',
views.AccountStatBalance.as_view(),
name = 'kfet.account.stat.balance.days'),
url('^accounts/(?P<trigramme>.{3})/stat/balance/anytime/$',
views.AccountStatBalance.as_view(),
name = 'kfet.account.stat.balance.anytime'),
name='kfet.account.stat.balance'),
# -----
# Checkout urls

View file

@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
from urllib.parse import urlencode
from django.shortcuts import render, get_object_or_404, redirect
from django.core.exceptions import PermissionDenied
from django.core.cache import cache
from django.views.generic import ListView, DetailView, TemplateView
from django.views.generic.list import BaseListView, MultipleObjectTemplateResponseMixin
from django.views.generic.detail import BaseDetailView, SingleObjectTemplateResponseMixin
from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView
from django.views.generic.detail import BaseDetailView
from django.views.generic.edit import CreateView, UpdateView
from django.core.urlresolvers import reverse, reverse_lazy
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
@ -2041,49 +2042,46 @@ class JSONDetailView(JSONResponseMixin, BaseDetailView):
return self.render_to_json_response(context)
class ObjectResumeStat(JSONDetailView):
class PkUrlMixin(object):
def get_object(self, *args, **kwargs):
get_by = self.kwargs.get(self.pk_url_kwarg)
return get_object_or_404(self.model, **{self.pk_url_kwarg: get_by})
class SingleResumeStat(JSONDetailView):
"""
Summarize all the stats of an object
Handles JSONResponse
"""
context_object_name = ''
id_prefix = ''
# nombre de vues à résumer
nb_stat = 2
# Le combienième est celui par defaut ?
# (entre 0 et nb_stat-1)
nb_default = 0
stat_labels = ['stat_1', 'stat_2']
stat_urls = ['url_1', 'url_2']
# sert à renverser les urls
# utile de le surcharger quand l'url prend d'autres arguments que l'id
def get_object_url_kwargs(self, **kwargs):
return {'pk': self.object.id}
def url_kwargs(self, **kwargs):
return [{}] * self.nb_stat
stats = []
url_stat = None
def get_context_data(self, **kwargs):
# On n'hérite pas
object_id = self.object.id
url_kwargs = self.url_kwargs()
context = {}
stats = {}
for i in range(self.nb_stat):
stats[i] = {
'label': self.stat_labels[i],
'btn': "btn_%s_%d_%d" % (self.id_prefix,
object_id,
i),
'url': reverse(self.stat_urls[i],
kwargs=dict(
self.get_object_url_kwargs(),
**url_kwargs[i]
),
),
}
prefix = "%s_%d" % (self.id_prefix, object_id)
stats = []
prefix = '{}_{}'.format(self.id_prefix, object_id)
for i, stat_def in enumerate(self.stats):
url_pk = getattr(self.object, self.pk_url_kwarg)
url_params_d = stat_def.get('url_params', {})
if len(url_params_d) > 0:
url_params = '?{}'.format(urlencode(url_params_d))
else:
url_params = ''
stats.append({
'label': stat_def['label'],
'btn': 'btn_{}_{}'.format(prefix, i),
'url': '{url}{params}'.format(
url=reverse(self.url_stat, args=[url_pk]),
params=url_params,
),
})
context['id_prefix'] = prefix
context['content_id'] = "content_%s" % prefix
context['stats'] = stats
@ -2100,39 +2098,47 @@ ID_PREFIX_ACC_BALANCE = "balance_acc"
# Un résumé de toutes les vues ArticleStatBalance
# REND DU JSON
class AccountStatBalanceAll(ObjectResumeStat):
class AccountStatBalanceList(PkUrlMixin, SingleResumeStat):
model = Account
context_object_name = 'account'
trigramme_url_kwarg = 'trigramme'
pk_url_kwarg = 'trigramme'
url_stat = 'kfet.account.stat.balance'
id_prefix = ID_PREFIX_ACC_BALANCE
nb_stat = 5
stats = [
{
'label': 'Tout le temps',
},
{
'label': '1 an',
'url_params': {'last_days': 365},
},
{
'label': '6 mois',
'url_params': {'last_days': 183},
},
{
'label': '3 mois',
'url_params': {'last_days': 90},
},
{
'label': '30 jours',
'url_params': {'last_days': 30},
},
]
nb_default = 0
stat_labels = ["Tout le temps", "1 an", "6 mois", "3 mois", "30 jours"]
stat_urls = ['kfet.account.stat.balance.anytime'] \
+ ['kfet.account.stat.balance.days'] * 4
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 url_kwargs(self, **kwargs):
context_list = (super(AccountStatBalanceAll, self)
.url_kwargs(**kwargs))
context_list[1] = {'nb_date': 365}
context_list[2] = {'nb_date': 183}
context_list[3] = {'nb_date': 90}
context_list[4] = {'nb_date': 30}
return context_list
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(AccountStatBalanceAll, self).dispatch(*args, **kwargs)
return super().dispatch(*args, **kwargs)
class AccountStatBalance(JSONDetailView):
class AccountStatBalance(PkUrlMixin, JSONDetailView):
"""
Returns a JSON containing the evolution a the personnal
balance of a trigramme between timezone.now() and `nb_days`
@ -2141,44 +2147,38 @@ class AccountStatBalance(JSONDetailView):
does not takes into account the balance offset
"""
model = Account
trigramme_url_kwarg = 'trigramme'
nb_date_url_kwargs = 'nb_date'
pk_url_kwarg = 'trigramme'
context_object_name = 'account'
id_prefix = ""
def get_object(self, **kwargs):
trigramme = self.kwargs.get(self.trigramme_url_kwarg)
return get_object_or_404(Account, trigramme=trigramme)
def get_changes_list(self, **kwargs):
def get_changes_list(self, last_days=None, begin_date=None, end_date=None):
account = self.object
nb_date = self.kwargs.get(self.nb_date_url_kwargs, None)
end_date = this_morning()
if nb_date is None:
begin_date = timezone.datetime(year=1980, month=1, day=1)
anytime = True
else:
begin_date = this_morning() \
- timezone.timedelta(days=int(nb_date))
anytime = False
# On récupère les opérations
# prepare filters
if end_date is None:
end_date = this_morning()
if last_days is not None:
begin_date = end_date - timezone.timedelta(days=last_days)
# prepare querysets
# TODO: retirer les opgroup dont tous les op sont annulées
opgroups = list(OperationGroup.objects
.filter(on_acc=account)
.filter(at__gte=begin_date)
.filter(at__lte=end_date))
# On récupère les transferts reçus
received_transfers = list(Transfer.objects
.filter(to_acc=account)
.filter(canceled_at=None)
.filter(group__at__gte=begin_date)
.filter(group__at__lte=end_date))
# On récupère les transferts émis
emitted_transfers = list(Transfer.objects
.filter(from_acc=account)
.filter(canceled_at=None)
.filter(group__at__gte=begin_date)
.filter(group__at__lte=end_date))
opegroups = OperationGroup.objects.filter(on_acc=account)
recv_transfers = Transfer.objects.filter(to_acc=account,
canceled_at=None)
sent_transfers = Transfer.objects.filter(from_acc=account,
canceled_at=None)
# apply filters
if begin_date is not None:
opegroups = opegroups.filter(at__gte=begin_date)
recv_transfers = recv_transfers.filter(group__at__gte=begin_date)
sent_transfers = sent_transfers.filter(group__at__gte=begin_date)
if end_date is not None:
opegroups = opegroups.filter(at__lte=end_date)
recv_transfers = recv_transfers.filter(group__at__lte=end_date)
sent_transfers = sent_transfers.filter(group__at__lte=end_date)
# On transforme tout ça en une liste de dictionnaires sous la forme
# {'at': date,
# 'amount': changement de la balance (négatif si diminue la balance,
@ -2188,72 +2188,74 @@ class AccountStatBalance(JSONDetailView):
# sera mis à jour lors d'une
# autre passe)
# }
actions = [
# Maintenant (à changer si on gère autre chose que now)
{
actions = []
if begin_date is not None:
actions.append({
'at': begin_date.isoformat(),
'amount': 0,
'label': "début",
'balance': 0,
})
if end_date is not None:
actions.append({
'at': end_date.isoformat(),
'amout': 0,
'amount': 0,
'label': "actuel",
'balance': 0,
}
] + [
})
actions += [
{
'at': op.at.isoformat(),
'amount': op.amount,
'label': str(op),
'at': ope_grp.at.isoformat(),
'amount': ope_grp.amount,
'label': str(ope_grp),
'balance': 0,
} for op in opgroups
} for ope_grp in opegroups
] + [
{
'at': tr.group.at.isoformat(),
'amount': tr.amount,
'label': "%d€: %s -> %s" % (tr.amount,
tr.from_acc.trigramme,
tr.to_acc.trigramme),
'label': str(tr),
'balance': 0,
} for tr in received_transfers
} for tr in recv_transfers
] + [
{
'at': tr.group.at.isoformat(),
'amount': -tr.amount,
'label': "%d€: %s -> %s" % (tr.amount,
tr.from_acc.trigramme,
tr.to_acc.trigramme),
'label': str(tr),
'balance': 0,
} for tr in emitted_transfers
} for tr in sent_transfers
]
if not anytime:
actions += [
# Date de début :
{
'at': begin_date.isoformat(),
'amount': 0,
'label': "début",
'balance': 0,
}
]
# Maintenant on trie la liste des actions par ordre du plus récent
# an plus ancien et on met à jour la balance
actions = sorted(actions, key=lambda k: k['at'], reverse=True)
actions[0]['balance'] = account.balance
for i in range(len(actions)-1):
actions[i+1]['balance'] = actions[i]['balance'] \
- actions[i+1]['amount']
actions[i+1]['balance'] = \
actions[i]['balance'] - actions[i+1]['amount']
return actions
def get_context_data(self, **kwargs):
def get_context_data(self, *args, **kwargs):
context = {}
changes = self.get_changes_list()
nb_days = self.kwargs.get(self.nb_date_url_kwargs, None)
if nb_days is None:
nb_days_string = 'anytime'
else:
nb_days_string = str(int(nb_days))
context['charts'] = [ { "color": "rgb(255, 99, 132)",
"label": "Balance",
"values": changes } ]
last_days = self.request.GET.get('last_days', None)
if last_days is not None:
last_days = int(last_days)
begin_date = self.request.GET.get('begin_date', None)
end_date = self.request.GET.get('end_date', None)
changes = self.get_changes_list(
last_days=last_days,
begin_date=begin_date, end_date=end_date,
)
context['charts'] = [{
"color": "rgb(255, 99, 132)",
"label": "Balance",
"values": changes,
}]
context['is_time_chart'] = True
context['min_date'] = changes[len(changes)-1]['at']
context['min_date'] = changes[-1]['at']
context['max_date'] = changes[0]['at']
# TODO: offset
return context
@ -2274,7 +2276,7 @@ ID_PREFIX_ACC_LAST_MONTHS = "last_months_acc"
# Un résumé de toutes les vues ArticleStatLast
# NE REND PAS DE JSON
class AccountStatLastAll(ObjectResumeStat):
class AccountStatLastAll(SingleResumeStat):
model = Account
context_object_name = 'account'
trigramme_url_kwarg = 'trigramme'
@ -2419,7 +2421,7 @@ ID_PREFIX_ART_LAST_MONTHS = "last_months_art"
# Un résumé de toutes les vues ArticleStatLast
# NE REND PAS DE JSON
class ArticleStatLastAll(ObjectResumeStat):
class ArticleStatLastAll(SingleResumeStat):
model = Article
context_object_name = 'article'
id_prefix = ID_PREFIX_ART_LAST