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(), views.AccountStatLastDay.as_view(),
name = 'kfet.account.stat.last.day'), name = 'kfet.account.stat.last.day'),
url('^accounts/(?P<trigramme>.{3})/stat/balance/$', url(r'^accounts/(?P<trigramme>.{3})/stat/balance/list$',
views.AccountStatBalanceAll.as_view(), views.AccountStatBalanceList.as_view(),
name = 'kfet.account.stat.balance'), name='kfet.account.stat.balance.list'),
url('^accounts/(?P<trigramme>.{3})/stat/balance/d/(?P<nb_date>\d*)/$', url(r'^accounts/(?P<trigramme>.{3})/stat/balance$',
views.AccountStatBalance.as_view(), views.AccountStatBalance.as_view(),
name = 'kfet.account.stat.balance.days'), name='kfet.account.stat.balance'),
url('^accounts/(?P<trigramme>.{3})/stat/balance/anytime/$',
views.AccountStatBalance.as_view(),
name = 'kfet.account.stat.balance.anytime'),
# ----- # -----
# Checkout urls # Checkout urls

View file

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