Merge branch 'aureplop/fix_stats' into 'master'
Stats K-Fêt - Python side See merge request !208
This commit is contained in:
commit
fe66a6ef6b
12 changed files with 624 additions and 597 deletions
|
@ -84,7 +84,7 @@ urlpatterns = [
|
|||
url(r'^k-fet/', include('kfet.urls')),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
if 'debug_toolbar' in settings.INSTALLED_APPS:
|
||||
import debug_toolbar
|
||||
urlpatterns += [
|
||||
url(r'^__debug__/', include(debug_toolbar.urls)),
|
||||
|
|
|
@ -491,24 +491,29 @@ class TransferGroup(models.Model):
|
|||
related_name = "+",
|
||||
blank = True, null = True, default = None)
|
||||
|
||||
|
||||
class Transfer(models.Model):
|
||||
group = models.ForeignKey(
|
||||
TransferGroup, on_delete = models.PROTECT,
|
||||
related_name = "transfers")
|
||||
TransferGroup, on_delete=models.PROTECT,
|
||||
related_name="transfers")
|
||||
from_acc = models.ForeignKey(
|
||||
Account, on_delete = models.PROTECT,
|
||||
related_name = "transfers_from")
|
||||
Account, on_delete=models.PROTECT,
|
||||
related_name="transfers_from")
|
||||
to_acc = models.ForeignKey(
|
||||
Account, on_delete = models.PROTECT,
|
||||
related_name = "transfers_to")
|
||||
amount = models.DecimalField(max_digits = 6, decimal_places = 2)
|
||||
Account, on_delete=models.PROTECT,
|
||||
related_name="transfers_to")
|
||||
amount = models.DecimalField(max_digits=6, decimal_places=2)
|
||||
# Optional
|
||||
canceled_by = models.ForeignKey(
|
||||
Account, on_delete = models.PROTECT,
|
||||
null = True, blank = True, default = None,
|
||||
related_name = "+")
|
||||
Account, on_delete=models.PROTECT,
|
||||
null=True, blank=True, default=None,
|
||||
related_name="+")
|
||||
canceled_at = models.DateTimeField(
|
||||
null = True, blank = True, default = None)
|
||||
null=True, blank=True, default=None)
|
||||
|
||||
def __str__(self):
|
||||
return '{} -> {}: {}€'.format(self.from_acc, self.to_acc, self.amount)
|
||||
|
||||
|
||||
class OperationGroup(models.Model):
|
||||
on_acc = models.ForeignKey(
|
||||
|
|
|
@ -106,6 +106,7 @@ textarea {
|
|||
|
||||
.panel-md-margin{
|
||||
background-color: white;
|
||||
overflow:hidden;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
padding-bottom: 15px;
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
(function($){
|
||||
window.StatsGroup = function (url, target) {
|
||||
// a class to properly display statictics
|
||||
|
||||
|
||||
// url : points to an ObjectResumeStat that lists the options through JSON
|
||||
// target : element of the DOM where to put the stats
|
||||
|
||||
|
||||
var self = this;
|
||||
var element = $(target);
|
||||
var content = $("<div>");
|
||||
|
@ -22,28 +22,29 @@
|
|||
return array;
|
||||
}
|
||||
|
||||
function handleTimeChart (dict) {
|
||||
function handleTimeChart (data) {
|
||||
// reads the balance data and put it into chartjs formatting
|
||||
var data = dictToArray(dict, 0);
|
||||
chart_data = new Array();
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var source = data[i];
|
||||
data[i] = { x: new Date(source.at),
|
||||
y: source.balance,
|
||||
label: source.label }
|
||||
chart_data[i] = {
|
||||
x: new Date(source.at),
|
||||
y: source.balance,
|
||||
label: source.label,
|
||||
}
|
||||
}
|
||||
return data;
|
||||
return chart_data;
|
||||
}
|
||||
|
||||
|
||||
function showStats () {
|
||||
// CALLBACK : called when a button is selected
|
||||
|
||||
|
||||
// shows the focus on the correct button
|
||||
buttons.find(".focus").removeClass("focus");
|
||||
$(this).addClass("focus");
|
||||
|
||||
// loads data and shows it
|
||||
$.getJSON(this.stats_target_url + "?format=json",
|
||||
displayStats);
|
||||
$.getJSON(this.stats_target_url, {format: 'json'}, displayStats);
|
||||
}
|
||||
|
||||
function displayStats (data) {
|
||||
|
@ -51,14 +52,14 @@
|
|||
|
||||
var chart_datasets = [];
|
||||
var charts = dictToArray(data.charts);
|
||||
|
||||
|
||||
// are the points indexed by timestamps?
|
||||
var is_time_chart = data.is_time_chart || false;
|
||||
|
||||
// reads the charts data
|
||||
for (var i = 0; i < charts.length; i++) {
|
||||
var chart = charts[i];
|
||||
|
||||
|
||||
// format the data
|
||||
var chart_data = is_time_chart ? handleTimeChart(chart.values) : dictToArray(chart.values, 1);
|
||||
|
||||
|
@ -78,6 +79,7 @@
|
|||
var chart_options =
|
||||
{
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
tooltips: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
|
@ -130,25 +132,25 @@
|
|||
type: 'line',
|
||||
options: chart_options,
|
||||
data: {
|
||||
labels: dictToArray(data.labels, 1),
|
||||
labels: (data.labels || []).slice(1),
|
||||
datasets: chart_datasets,
|
||||
}
|
||||
};
|
||||
|
||||
// saves the previous charts to be destroyed
|
||||
var prev_chart = content.children();
|
||||
|
||||
|
||||
// clean
|
||||
prev_chart.remove();
|
||||
|
||||
// creates a blank canvas element and attach it to the DOM
|
||||
var canvas = $("<canvas>");
|
||||
var canvas = $("<canvas height='250'>");
|
||||
content.append(canvas);
|
||||
|
||||
// create the chart
|
||||
var chart = new Chart(canvas, chart_model);
|
||||
|
||||
// clean
|
||||
prev_chart.remove();
|
||||
}
|
||||
|
||||
|
||||
// initialize the interface
|
||||
function initialize (data) {
|
||||
// creates the bar with the buttons
|
||||
|
@ -158,8 +160,8 @@
|
|||
"aria-label": "select-period"});
|
||||
|
||||
var to_click;
|
||||
var context = dictToArray(data.stats);
|
||||
|
||||
var context = data.stats;
|
||||
|
||||
for (var i = 0; i < context.length; i++) {
|
||||
// creates the button
|
||||
var btn_wrapper = $("<div>",
|
||||
|
@ -191,7 +193,7 @@
|
|||
|
||||
// constructor
|
||||
(function () {
|
||||
$.getJSON(url + "?format=json", initialize);
|
||||
$.getJSON(url, {format: 'json'}, initialize);
|
||||
})();
|
||||
};
|
||||
})(jQuery);
|
||||
|
|
|
@ -1,98 +1,155 @@
|
|||
# -*- 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 = 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
|
||||
KFET_WAKES_UP_AT = time(7, 0)
|
||||
|
||||
|
||||
# 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 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)
|
||||
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
|
||||
# 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 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]
|
||||
|
||||
|
||||
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 DayScale(Scale):
|
||||
name = 'day'
|
||||
step = timedelta(days=1)
|
||||
label_fmt = '%A'
|
||||
|
||||
@classmethod
|
||||
def get_chunk_start(cls, dt):
|
||||
return to_kfet_day(dt)
|
||||
|
||||
|
||||
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 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
|
||||
|
||||
|
||||
def this_first_month_day():
|
||||
now = timezone.now()
|
||||
first_day = timezone.datetime(year=now.year,
|
||||
month=now.month,
|
||||
day=1,
|
||||
hour=KFET_WAKES_UP_AT)
|
||||
return first_day
|
||||
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 this_monday_morning():
|
||||
now = timezone.now()
|
||||
monday = now - timezone.timedelta(days=now.isoweekday()-1)
|
||||
monday_morning = timezone.datetime(year=monday.year,
|
||||
month=monday.month,
|
||||
day=monday.day,
|
||||
hour=KFET_WAKES_UP_AT)
|
||||
return monday_morning
|
||||
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 this_morning():
|
||||
now = timezone.now()
|
||||
morning = timezone.datetime(year=now.year,
|
||||
month=now.month,
|
||||
day=now.day,
|
||||
hour=KFET_WAKES_UP_AT)
|
||||
return morning
|
||||
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
|
||||
|
@ -100,3 +157,78 @@ def this_morning():
|
|||
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
|
||||
]
|
||||
|
|
|
@ -13,12 +13,17 @@
|
|||
{% if account.user == request.user %}
|
||||
<script type="text/javascript" src="{% static 'kfet/js/Chart.bundle.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'kfet/js/statistic.js' %}"></script>
|
||||
<script>
|
||||
jQuery(document).ready(function() {
|
||||
var stat_last = new StatsGroup("{% url 'kfet.account.stat.last' trigramme=account.trigramme %}",
|
||||
$("#stat_last"));
|
||||
var stat_balance = new StatsGroup("{% url 'kfet.account.stat.balance' trigramme=account.trigramme %}",
|
||||
$("#stat_balance"));
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
var stat_last = new StatsGroup(
|
||||
"{% url 'kfet.account.stat.operation.list' trigramme=account.trigramme %}",
|
||||
$("#stat_last"),
|
||||
);
|
||||
var stat_balance = new StatsGroup(
|
||||
"{% url 'kfet.account.stat.balance.list' trigramme=account.trigramme %}",
|
||||
$("#stat_balance"),
|
||||
);
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
|
@ -66,22 +71,22 @@
|
|||
{% if account.user == request.user %}
|
||||
<div class="content-right-block content-right-block-transparent">
|
||||
<h2>Statistiques</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 nopadding">
|
||||
<div class="panel-md-margin">
|
||||
<h3>Ma balance</h3>
|
||||
<div id="stat_balance" class"stat-graph"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 nopadding">
|
||||
<div class="panel-md-margin">
|
||||
<h3>Ma balance</h3>
|
||||
<div id="stat_balance"></div>
|
||||
</div>
|
||||
</div><!-- /row -->
|
||||
<div class="row">
|
||||
<div class="col-sm-12 nopadding">
|
||||
<div class="panel-md-margin">
|
||||
<h3>Ma consommation</h3>
|
||||
<div id="stat_last" class"stat-graph"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /row -->
|
||||
<div class="row">
|
||||
<div class="col-sm-12 nopadding">
|
||||
<div class="panel-md-margin">
|
||||
<h3>Ma consommation</h3>
|
||||
<div id="stat_last"></div>
|
||||
</div>
|
||||
</div><!-- /row -->
|
||||
</div>
|
||||
</div><!-- /row -->
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="content-right-block">
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
{% if account.user == request.user %}
|
||||
Mon compte
|
||||
{% else %}
|
||||
Informations du compte {{ account.trigramme }}
|
||||
{% endif %}
|
|
@ -1,6 +1,11 @@
|
|||
{% extends 'kfet/base.html' %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script type="text/javascript" src="{% static 'kfet/js/Chart.bundle.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'kfet/js/statistic.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}Informations sur l'article {{ article }}{% endblock %}
|
||||
{% block content-header-title %}Article - {{ article.name }}{% endblock %}
|
||||
|
||||
|
@ -82,27 +87,26 @@
|
|||
</div>
|
||||
<div class="content-right-block content-right-block-transparent">
|
||||
<h2>Statistiques</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 nopadding">
|
||||
<div class="panel-md-margin">
|
||||
<h3>Ventes de {{ article.name }}</h3>
|
||||
<div id="stat_last"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 nopadding">
|
||||
<div class="panel-md-margin">
|
||||
<h3>Ventes de {{ article.name }}</h3>
|
||||
<div id="stat_last"></div>
|
||||
</div>
|
||||
</div><!-- /row -->
|
||||
</div>
|
||||
</div><!-- /row -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script type="text/javascript" src="{% static 'kfet/js/Chart.bundle.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'kfet/js/statistic.js' %}"></script>
|
||||
<script>
|
||||
jQuery(document).ready(function() {
|
||||
var stat_last = new StatsGroup("{% url 'kfet.article.stat.last' article.id %}",
|
||||
$("#stat_last"));
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
var stat_last = new StatsGroup(
|
||||
"{% url 'kfet.article.stat.sales.list' article.id %}",
|
||||
$("#stat_last"),
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,5 +1,70 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.test import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
# Écrire les tests ici
|
||||
from django.test import TestCase, Client
|
||||
from django.contrib.auth.models import User, Permission
|
||||
|
||||
from .models import Account, Article, ArticleCategory
|
||||
|
||||
|
||||
class TestStats(TestCase):
|
||||
@patch('kfet.signals.messages')
|
||||
def test_user_stats(self, mock_messages):
|
||||
"""
|
||||
Checks that we can get the stat-related pages without any problem.
|
||||
"""
|
||||
# We setup two users and an article. Only the first user is part of the
|
||||
# team.
|
||||
user = User.objects.create(username="Foobar")
|
||||
user.set_password("foobar")
|
||||
user.save()
|
||||
Account.objects.create(trigramme="FOO", cofprofile=user.profile)
|
||||
perm = Permission.objects.get(codename="is_team")
|
||||
user.user_permissions.add(perm)
|
||||
|
||||
user2 = User.objects.create(username="Barfoo")
|
||||
user2.set_password("barfoo")
|
||||
user2.save()
|
||||
Account.objects.create(trigramme="BAR", cofprofile=user2.profile)
|
||||
|
||||
article = Article.objects.create(
|
||||
name="article",
|
||||
category=ArticleCategory.objects.create(name="C")
|
||||
)
|
||||
|
||||
# Each user have its own client
|
||||
client = Client()
|
||||
client.login(username="Foobar", password="foobar")
|
||||
client2 = Client()
|
||||
client2.login(username="Barfoo", password="barfoo")
|
||||
|
||||
# 1. FOO should be able to get these pages but BAR receives a Forbidden
|
||||
# response
|
||||
user_urls = [
|
||||
"/k-fet/accounts/FOO/stat/operations/list",
|
||||
"/k-fet/accounts/FOO/stat/operations?{}".format(
|
||||
'&'.join(["scale=day",
|
||||
"types=['purchase']",
|
||||
"scale_args={'n_steps':+7,+'last':+True}",
|
||||
"format=json"])),
|
||||
"/k-fet/accounts/FOO/stat/balance/list",
|
||||
"/k-fet/accounts/FOO/stat/balance?format=json"
|
||||
]
|
||||
for url in user_urls:
|
||||
resp = client.get(url)
|
||||
self.assertEqual(200, resp.status_code)
|
||||
resp2 = client2.get(url)
|
||||
self.assertEqual(403, resp2.status_code)
|
||||
|
||||
# 2. FOO is a member of the team and can get these pages but BAR
|
||||
# receives a Redirect response
|
||||
articles_urls = [
|
||||
"/k-fet/articles/{}/stat/sales/list".format(article.pk),
|
||||
"/k-fet/articles/{}/stat/sales".format(article.pk)
|
||||
]
|
||||
for url in articles_urls:
|
||||
resp = client.get(url)
|
||||
self.assertEqual(200, resp.status_code)
|
||||
resp2 = client2.get(url, follow=True)
|
||||
self.assertRedirects(resp2, "/")
|
||||
|
|
51
kfet/urls.py
51
kfet/urls.py
|
@ -69,28 +69,19 @@ urlpatterns = [
|
|||
name='kfet.account.negative'),
|
||||
|
||||
# Account - Statistics
|
||||
url('^accounts/(?P<trigramme>.{3})/stat/last/$',
|
||||
views.AccountStatLastAll.as_view(),
|
||||
name = 'kfet.account.stat.last'),
|
||||
url('^accounts/(?P<trigramme>.{3})/stat/last/month/$',
|
||||
views.AccountStatLastMonth.as_view(),
|
||||
name = 'kfet.account.stat.last.month'),
|
||||
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('^accounts/(?P<trigramme>.{3})/stat/operations/list$',
|
||||
views.AccountStatOperationList.as_view(),
|
||||
name='kfet.account.stat.operation.list'),
|
||||
url('^accounts/(?P<trigramme>.{3})/stat/operations$',
|
||||
views.AccountStatOperation.as_view(),
|
||||
name='kfet.account.stat.operation'),
|
||||
|
||||
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
|
||||
|
@ -149,20 +140,14 @@ urlpatterns = [
|
|||
# Article - Update
|
||||
url('^articles/(?P<pk>\d+)/edit$',
|
||||
teamkfet_required(views.ArticleUpdate.as_view()),
|
||||
name = 'kfet.article.update'),
|
||||
name='kfet.article.update'),
|
||||
# Article - Statistics
|
||||
url('^articles/(?P<pk>\d+)/stat/last/$',
|
||||
views.ArticleStatLastAll.as_view(),
|
||||
name = 'kfet.article.stat.last'),
|
||||
url('^articles/(?P<pk>\d+)/stat/last/month/$',
|
||||
views.ArticleStatLastMonth.as_view(),
|
||||
name = 'kfet.article.stat.last.month'),
|
||||
url('^articles/(?P<pk>\d+)/stat/last/week/$',
|
||||
views.ArticleStatLastWeek.as_view(),
|
||||
name = 'kfet.article.stat.last.week'),
|
||||
url('^articles/(?P<pk>\d+)/stat/last/day/$',
|
||||
views.ArticleStatLastDay.as_view(),
|
||||
name = 'kfet.article.stat.last.day'),
|
||||
url(r'^articles/(?P<pk>\d+)/stat/sales/list$',
|
||||
views.ArticleStatSalesList.as_view(),
|
||||
name='kfet.article.stat.sales.list'),
|
||||
url(r'^articles/(?P<pk>\d+)/stat/sales$',
|
||||
views.ArticleStatSales.as_view(),
|
||||
name='kfet.article.stat.sales'),
|
||||
|
||||
# -----
|
||||
# K-Psul urls
|
||||
|
|
654
kfet/views.py
654
kfet/views.py
|
@ -1,12 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import ast
|
||||
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
|
||||
|
@ -46,10 +48,8 @@ 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 ScaleMixin, last_stats_manifest, tot_ventes
|
||||
|
||||
|
||||
class Home(TemplateView):
|
||||
template_name = "kfet/home.html"
|
||||
|
@ -2029,87 +2029,54 @@ class JSONResponseMixin(object):
|
|||
return context
|
||||
|
||||
|
||||
class JSONDetailView(JSONResponseMixin,
|
||||
BaseDetailView):
|
||||
"""
|
||||
Returns a DetailView that renders a JSON
|
||||
"""
|
||||
class JSONDetailView(JSONResponseMixin, BaseDetailView):
|
||||
"""Returns a DetailView that renders a JSON."""
|
||||
|
||||
def render_to_response(self, context):
|
||||
return self.render_to_json_response(context)
|
||||
|
||||
class HybridDetailView(JSONResponseMixin,
|
||||
SingleObjectTemplateResponseMixin,
|
||||
BaseDetailView):
|
||||
"""
|
||||
Returns a DetailView as an html page except if a JSON file is requested
|
||||
by the GET method in which case it returns a JSON response.
|
||||
"""
|
||||
def render_to_response(self, context):
|
||||
# Look for a 'format=json' GET argument
|
||||
if self.request.GET.get('format') == 'json':
|
||||
return self.render_to_json_response(context)
|
||||
else:
|
||||
return super(HybridDetailView, self).render_to_response(context)
|
||||
|
||||
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 HybridListView(JSONResponseMixin,
|
||||
MultipleObjectTemplateResponseMixin,
|
||||
BaseListView):
|
||||
"""
|
||||
Returns a ListView as an html page except if a JSON file is requested
|
||||
by the GET method in which case it returns a JSON response.
|
||||
"""
|
||||
def render_to_response(self, context):
|
||||
# Look for a 'format=json' GET argument
|
||||
if self.request.GET.get('format') == 'json':
|
||||
return self.render_to_json_response(context)
|
||||
else:
|
||||
return super(HybridListView, self).render_to_response(context)
|
||||
class SingleResumeStat(JSONDetailView):
|
||||
"""Manifest for a kind of a stat about an object.
|
||||
|
||||
Returns JSON whose payload is an array containing descriptions of a stat:
|
||||
url to retrieve data, label, ...
|
||||
|
||||
class ObjectResumeStat(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
|
||||
|
@ -2124,87 +2091,84 @@ class ObjectResumeStat(JSONDetailView):
|
|||
ID_PREFIX_ACC_BALANCE = "balance_acc"
|
||||
|
||||
|
||||
# Un résumé de toutes les vues ArticleStatBalance
|
||||
# REND DU JSON
|
||||
class AccountStatBalanceAll(ObjectResumeStat):
|
||||
class AccountStatBalanceList(PkUrlMixin, SingleResumeStat):
|
||||
"""Manifest for balance stats of an account."""
|
||||
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):
|
||||
"""
|
||||
Returns a JSON containing the evolution a the personnal
|
||||
balance of a trigramme between timezone.now() and `nb_days`
|
||||
ago (specified to the view as an argument)
|
||||
takes into account the Operations and the Transfers
|
||||
does not takes into account the balance offset
|
||||
class AccountStatBalance(PkUrlMixin, JSONDetailView):
|
||||
"""Datasets of balance of an account.
|
||||
|
||||
Operations and Transfers are taken into account.
|
||||
|
||||
"""
|
||||
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 last_days is not None:
|
||||
end_date = timezone.now()
|
||||
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,
|
||||
|
@ -2214,76 +2178,86 @@ class AccountStatBalance(JSONDetailView):
|
|||
# sera mis à jour lors d'une
|
||||
# autre passe)
|
||||
# }
|
||||
actions = [
|
||||
# Maintenant (à changer si on gère autre chose que now)
|
||||
|
||||
actions = []
|
||||
|
||||
actions.append({
|
||||
'at': (begin_date or account.created_at).isoformat(),
|
||||
'amount': 0,
|
||||
'label': 'début',
|
||||
'balance': 0,
|
||||
})
|
||||
actions.append({
|
||||
'at': (end_date or timezone.now()).isoformat(),
|
||||
'amount': 0,
|
||||
'label': 'fin',
|
||||
'balance': 0,
|
||||
})
|
||||
|
||||
actions += [
|
||||
{
|
||||
'at': end_date.isoformat(),
|
||||
'amout': 0,
|
||||
'label': "actuel",
|
||||
'at': ope_grp.at.isoformat(),
|
||||
'amount': ope_grp.amount,
|
||||
'label': str(ope_grp),
|
||||
'balance': 0,
|
||||
}
|
||||
] + [
|
||||
{
|
||||
'at': op.at.isoformat(),
|
||||
'amount': op.amount,
|
||||
'label': str(op),
|
||||
'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']
|
||||
if len(actions) > 1:
|
||||
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']
|
||||
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['max_date'] = changes[0]['at']
|
||||
if len(changes) > 0:
|
||||
context['min_date'] = changes[-1]['at']
|
||||
context['max_date'] = changes[0]['at']
|
||||
# TODO: offset
|
||||
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(AccountStatBalance, self).dispatch(*args, **kwargs)
|
||||
|
@ -2298,140 +2272,77 @@ ID_PREFIX_ACC_LAST_WEEKS = "last_weeks_acc"
|
|||
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 AccountStatOperationList(PkUrlMixin, SingleResumeStat):
|
||||
"""Manifest for operations stats of an account."""
|
||||
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 = last_stats_manifest(types=[Operation.PURCHASE])
|
||||
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):
|
||||
"""
|
||||
Returns a JSON containing the evolution a the personnal
|
||||
consommation of a trigramme at the diffent dates specified
|
||||
"""
|
||||
class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView):
|
||||
"""Datasets of operations of an account."""
|
||||
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, scale, types=None):
|
||||
# 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(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])
|
||||
)
|
||||
return operations
|
||||
if types is not None:
|
||||
all_operations = all_operations.filter(type__in=types)
|
||||
chunks = self.chunkify_qs(all_operations, scale, field='group__at')
|
||||
return chunks
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {}
|
||||
nb_ventes = {}
|
||||
# On récupère les labels des dates
|
||||
context['labels'] = self.get_labels().copy()
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
old_ctx = super().get_context_data(*args, **kwargs)
|
||||
context = {'labels': old_ctx['labels']}
|
||||
scale = self.scale
|
||||
|
||||
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 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['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)
|
||||
|
||||
|
||||
# ------------------------
|
||||
|
@ -2443,143 +2354,64 @@ ID_PREFIX_ART_LAST_WEEKS = "last_weeks_art"
|
|||
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 ArticleStatSalesList(SingleResumeStat):
|
||||
"""Manifest for sales stats of an article."""
|
||||
model = Article
|
||||
context_object_name = 'article'
|
||||
id_prefix = ID_PREFIX_ART_LAST
|
||||
nb_stat = 3
|
||||
nb_default = 2
|
||||
stat_labels = ["Derniers mois", "Dernières semaines", "Derniers jours"]
|
||||
stat_urls = ['kfet.article.stat.last.month',
|
||||
'kfet.article.stat.last.week',
|
||||
'kfet.article.stat.last.day']
|
||||
url_stat = 'kfet.article.stat.sales'
|
||||
stats = last_stats_manifest()
|
||||
|
||||
@method_decorator(login_required)
|
||||
@method_decorator(teamkfet_required)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super(ArticleStatLastAll, self).dispatch(*args, **kwargs)
|
||||
return super().dispatch(*args, **kwargs)
|
||||
|
||||
|
||||
class ArticleStatLast(JSONDetailView):
|
||||
"""
|
||||
Returns a JSON containing the consommation
|
||||
of an article at the diffent dates precised
|
||||
"""
|
||||
class ArticleStatSales(ScaleMixin, JSONDetailView):
|
||||
"""Datasets of sales of an article."""
|
||||
model = Article
|
||||
context_object_name = 'article'
|
||||
end_date = timezone.now()
|
||||
id_prefix = ""
|
||||
|
||||
def render_to_response(self, context):
|
||||
# Look for a 'format=json' GET argument
|
||||
if self.request.GET.get('format') == 'json':
|
||||
return self.render_to_json_response(context)
|
||||
else:
|
||||
return super(ArticleStatLast, self).render_to_response(context)
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
old_ctx = super().get_context_data(*args, **kwargs)
|
||||
context = {'labels': old_ctx['labels']}
|
||||
scale = self.scale
|
||||
|
||||
# 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):
|
||||
pass
|
||||
|
||||
# doit rendre un dictionnaire des labels
|
||||
# le dernier label ne sera pas utilisé
|
||||
def get_labels(self, **kwargs):
|
||||
pass
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {}
|
||||
# On récupère les labels des dates
|
||||
context['labels'] = self.get_labels().copy()
|
||||
# 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
|
||||
# à 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(article=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])
|
||||
)
|
||||
all_operations = (
|
||||
Operation.objects
|
||||
.filter(type=Operation.PURCHASE,
|
||||
article=self.object,
|
||||
canceled_at=None,
|
||||
)
|
||||
)
|
||||
chunks = self.chunkify_qs(all_operations, scale, field='group__at')
|
||||
# On compte les opérations
|
||||
nb_ventes = {}
|
||||
nb_accounts = {}
|
||||
nb_liq = {}
|
||||
for i in operations:
|
||||
nb_ventes[i] = tot_ventes(operations[i])
|
||||
nb_liq[i] = tot_ventes(
|
||||
operations[i]
|
||||
.filter(group__on_acc__trigramme='LIQ')
|
||||
)
|
||||
nb_accounts[i] = tot_ventes(
|
||||
operations[i]
|
||||
.exclude(group__on_acc__trigramme='LIQ')
|
||||
)
|
||||
context['charts'] = [ { "color": "rgb(255, 99, 132)",
|
||||
"label": "Toutes consommations",
|
||||
"values": nb_ventes },
|
||||
{ "color": "rgb(54, 162, 235)",
|
||||
"label": "LIQ",
|
||||
"values": nb_liq },
|
||||
{ "color": "rgb(255, 205, 86)",
|
||||
"label": "Comptes K-Fêt",
|
||||
"values": nb_accounts } ]
|
||||
nb_ventes = []
|
||||
nb_accounts = []
|
||||
nb_liq = []
|
||||
for qs in chunks:
|
||||
nb_ventes.append(
|
||||
tot_ventes(qs))
|
||||
nb_liq.append(
|
||||
tot_ventes(qs.filter(group__on_acc__trigramme='LIQ')))
|
||||
nb_accounts.append(
|
||||
tot_ventes(qs.exclude(group__on_acc__trigramme='LIQ')))
|
||||
context['charts'] = [{"color": "rgb(255, 99, 132)",
|
||||
"label": "Toutes consommations",
|
||||
"values": nb_ventes},
|
||||
{"color": "rgb(54, 162, 235)",
|
||||
"label": "LIQ",
|
||||
"values": nb_liq},
|
||||
{"color": "rgb(255, 205, 86)",
|
||||
"label": "Comptes K-Fêt",
|
||||
"values": nb_accounts}]
|
||||
return context
|
||||
|
||||
@method_decorator(login_required)
|
||||
@method_decorator(teamkfet_required)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super(ArticleStatLast, self).dispatch(*args, **kwargs)
|
||||
|
||||
|
||||
# Rend les ventes des 7 derniers jours
|
||||
# Aujourd'hui non compris
|
||||
class ArticleStatLastDay(ArticleStatLast):
|
||||
end_date = this_morning()
|
||||
id_prefix = ID_PREFIX_ART_LAST_DAYS
|
||||
|
||||
def get_dates(self, **kwargs):
|
||||
return lastdays(7)
|
||||
|
||||
def get_labels(self, **kwargs):
|
||||
days = lastdays(7)
|
||||
return daynames(days)
|
||||
|
||||
|
||||
# Rend les ventes de 7 dernières semaines
|
||||
# La semaine en cours n'est pas comprise
|
||||
class ArticleStatLastWeek(ArticleStatLast):
|
||||
end_date = this_monday_morning()
|
||||
id_prefix = ID_PREFIX_ART_LAST_WEEKS
|
||||
|
||||
def get_dates(self, **kwargs):
|
||||
return lastweeks(7)
|
||||
|
||||
def get_labels(self, **kwargs):
|
||||
weeks = lastweeks(7)
|
||||
return weeknames(weeks)
|
||||
|
||||
|
||||
# Rend les ventes des 7 derniers mois
|
||||
# Le mois en cours n'est pas compris
|
||||
class ArticleStatLastMonth(ArticleStatLast):
|
||||
end_date = this_monday_morning()
|
||||
id_prefix = ID_PREFIX_ART_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)
|
||||
|
|
|
@ -20,3 +20,4 @@ django-widget-tweaks==1.4.1
|
|||
git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_custommail
|
||||
ldap3
|
||||
git+https://github.com/Aureplop/channels.git#egg=channels
|
||||
python-dateutil
|
||||
|
|
Loading…
Reference in a new issue