Merge branch 'aureplop/fix_stats' into 'master'

Stats K-Fêt - Python side

See merge request !208
This commit is contained in:
Martin Pepin 2017-04-05 00:41:47 +02:00
commit fe66a6ef6b
12 changed files with 624 additions and 597 deletions

View file

@ -84,7 +84,7 @@ urlpatterns = [
url(r'^k-fet/', include('kfet.urls')), url(r'^k-fet/', include('kfet.urls')),
] ]
if settings.DEBUG: if 'debug_toolbar' in settings.INSTALLED_APPS:
import debug_toolbar import debug_toolbar
urlpatterns += [ urlpatterns += [
url(r'^__debug__/', include(debug_toolbar.urls)), url(r'^__debug__/', include(debug_toolbar.urls)),

View file

@ -491,24 +491,29 @@ class TransferGroup(models.Model):
related_name = "+", related_name = "+",
blank = True, null = True, default = None) blank = True, null = True, default = None)
class Transfer(models.Model): class Transfer(models.Model):
group = models.ForeignKey( group = models.ForeignKey(
TransferGroup, on_delete = models.PROTECT, TransferGroup, on_delete=models.PROTECT,
related_name = "transfers") related_name="transfers")
from_acc = models.ForeignKey( from_acc = models.ForeignKey(
Account, on_delete = models.PROTECT, Account, on_delete=models.PROTECT,
related_name = "transfers_from") related_name="transfers_from")
to_acc = models.ForeignKey( to_acc = models.ForeignKey(
Account, on_delete = models.PROTECT, Account, on_delete=models.PROTECT,
related_name = "transfers_to") related_name="transfers_to")
amount = models.DecimalField(max_digits = 6, decimal_places = 2) amount = models.DecimalField(max_digits=6, decimal_places=2)
# Optional # Optional
canceled_by = models.ForeignKey( canceled_by = models.ForeignKey(
Account, on_delete = models.PROTECT, Account, on_delete=models.PROTECT,
null = True, blank = True, default = None, null=True, blank=True, default=None,
related_name = "+") related_name="+")
canceled_at = models.DateTimeField( 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): class OperationGroup(models.Model):
on_acc = models.ForeignKey( on_acc = models.ForeignKey(

View file

@ -106,6 +106,7 @@ textarea {
.panel-md-margin{ .panel-md-margin{
background-color: white; background-color: white;
overflow:hidden;
padding-left: 15px; padding-left: 15px;
padding-right: 15px; padding-right: 15px;
padding-bottom: 15px; padding-bottom: 15px;

View file

@ -1,10 +1,10 @@
(function($){ (function($){
window.StatsGroup = function (url, target) { window.StatsGroup = function (url, target) {
// a class to properly display statictics // a class to properly display statictics
// url : points to an ObjectResumeStat that lists the options through JSON // url : points to an ObjectResumeStat that lists the options through JSON
// target : element of the DOM where to put the stats // target : element of the DOM where to put the stats
var self = this; var self = this;
var element = $(target); var element = $(target);
var content = $("<div>"); var content = $("<div>");
@ -22,28 +22,29 @@
return array; return array;
} }
function handleTimeChart (dict) { function handleTimeChart (data) {
// reads the balance data and put it into chartjs formatting // 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++) { for (var i = 0; i < data.length; i++) {
var source = data[i]; var source = data[i];
data[i] = { x: new Date(source.at), chart_data[i] = {
y: source.balance, x: new Date(source.at),
label: source.label } y: source.balance,
label: source.label,
}
} }
return data; return chart_data;
} }
function showStats () { function showStats () {
// CALLBACK : called when a button is selected // CALLBACK : called when a button is selected
// shows the focus on the correct button // shows the focus on the correct button
buttons.find(".focus").removeClass("focus"); buttons.find(".focus").removeClass("focus");
$(this).addClass("focus"); $(this).addClass("focus");
// loads data and shows it // loads data and shows it
$.getJSON(this.stats_target_url + "?format=json", $.getJSON(this.stats_target_url, {format: 'json'}, displayStats);
displayStats);
} }
function displayStats (data) { function displayStats (data) {
@ -51,14 +52,14 @@
var chart_datasets = []; var chart_datasets = [];
var charts = dictToArray(data.charts); var charts = dictToArray(data.charts);
// are the points indexed by timestamps? // are the points indexed by timestamps?
var is_time_chart = data.is_time_chart || false; var is_time_chart = data.is_time_chart || false;
// reads the charts data // reads the charts data
for (var i = 0; i < charts.length; i++) { for (var i = 0; i < charts.length; i++) {
var chart = charts[i]; var chart = charts[i];
// format the data // format the data
var chart_data = is_time_chart ? handleTimeChart(chart.values) : dictToArray(chart.values, 1); var chart_data = is_time_chart ? handleTimeChart(chart.values) : dictToArray(chart.values, 1);
@ -78,6 +79,7 @@
var chart_options = var chart_options =
{ {
responsive: true, responsive: true,
maintainAspectRatio: false,
tooltips: { tooltips: {
mode: 'index', mode: 'index',
intersect: false, intersect: false,
@ -130,25 +132,25 @@
type: 'line', type: 'line',
options: chart_options, options: chart_options,
data: { data: {
labels: dictToArray(data.labels, 1), labels: (data.labels || []).slice(1),
datasets: chart_datasets, datasets: chart_datasets,
} }
}; };
// saves the previous charts to be destroyed // saves the previous charts to be destroyed
var prev_chart = content.children(); var prev_chart = content.children();
// clean
prev_chart.remove();
// creates a blank canvas element and attach it to the DOM // creates a blank canvas element and attach it to the DOM
var canvas = $("<canvas>"); var canvas = $("<canvas height='250'>");
content.append(canvas); content.append(canvas);
// create the chart // create the chart
var chart = new Chart(canvas, chart_model); var chart = new Chart(canvas, chart_model);
// clean
prev_chart.remove();
} }
// initialize the interface // initialize the interface
function initialize (data) { function initialize (data) {
// creates the bar with the buttons // creates the bar with the buttons
@ -158,8 +160,8 @@
"aria-label": "select-period"}); "aria-label": "select-period"});
var to_click; var to_click;
var context = dictToArray(data.stats); var context = data.stats;
for (var i = 0; i < context.length; i++) { for (var i = 0; i < context.length; i++) {
// creates the button // creates the button
var btn_wrapper = $("<div>", var btn_wrapper = $("<div>",
@ -191,7 +193,7 @@
// constructor // constructor
(function () { (function () {
$.getJSON(url + "?format=json", initialize); $.getJSON(url, {format: 'json'}, initialize);
})(); })();
}; };
})(jQuery); })(jQuery);

View file

@ -1,98 +1,155 @@
# -*- coding: utf-8 -*- # -*- 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.utils import timezone
from django.db.models import Sum from django.db.models import Sum
KFET_WAKES_UP_AT = 7 KFET_WAKES_UP_AT = time(7, 0)
# 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
# donne le nom des semaines une liste de dates def kfet_day(year, month, day, start_at=KFET_WAKES_UP_AT):
# dans un dico ordonné """datetime wrapper with time offset."""
def weeknames(dates): return datetime.combine(date(year, month, day), start_at)
names = {}
for i in dates:
names[i] = dates[i].strftime("Semaine %W")
return names
# donne le nom des mois d'une liste de dates def to_kfet_day(dt, start_at=KFET_WAKES_UP_AT):
# dans un dico ordonné kfet_dt = kfet_day(year=dt.year, month=dt.month, day=dt.day)
def monthnames(dates): if dt.time() < start_at:
names = {} kfet_dt -= timedelta(days=1)
for i in dates: return kfet_dt
names[i] = dates[i].strftime("%B")
return names
# rend les dates des nb derniers jours class Scale(object):
# dans l'ordre chronologique name = None
# aujourd'hui compris step = None
# nb = 1 : rend hier
def lastdays(nb): def __init__(self, n_steps=0, begin=None, end=None,
morning = this_morning() last=False, std_chunk=True):
days = {} self.std_chunk = std_chunk
for i in range(1, nb+1): if last:
days[i] = morning - timezone.timedelta(days=nb - i + 1) end = timezone.now()
return days
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): class DayScale(Scale):
monday_morning = this_monday_morning() name = 'day'
mondays = {} step = timedelta(days=1)
for i in range(1, nb+1): label_fmt = '%A'
mondays[i] = monday_morning \
- timezone.timedelta(days=7*(nb - i + 1)) @classmethod
return mondays def get_chunk_start(cls, dt):
return to_kfet_day(dt)
def lastmonths(nb): class WeekScale(Scale):
first_month_day = this_first_month_day() name = 'week'
first_days = {} step = timedelta(days=7)
this_year = first_month_day.year label_fmt = 'Semaine %W'
this_month = first_month_day.month
for i in range(1, nb+1): @classmethod
month = ((this_month - 1 - (nb - i)) % 12) + 1 def get_chunk_start(cls, dt):
year = this_year + (nb - i) // 12 dt_kfet = to_kfet_day(dt)
first_days[i] = timezone.datetime(year=year, offset = timedelta(days=dt_kfet.weekday())
month=month, return dt_kfet - offset
day=1,
hour=KFET_WAKES_UP_AT)
return first_days
def this_first_month_day(): class MonthScale(Scale):
now = timezone.now() name = 'month'
first_day = timezone.datetime(year=now.year, step = relativedelta(months=1)
month=now.month, label_fmt = '%B'
day=1,
hour=KFET_WAKES_UP_AT) @classmethod
return first_day def get_chunk_start(cls, dt):
return to_kfet_day(dt).replace(day=1)
def this_monday_morning(): def stat_manifest(scales_def=None, scale_args=None, scale_prefix=None,
now = timezone.now() **other_url_params):
monday = now - timezone.timedelta(days=now.isoweekday()-1) if scale_prefix is None:
monday_morning = timezone.datetime(year=monday.year, scale_prefix = 'scale_'
month=monday.month, if scales_def is None:
day=monday.day, scales_def = []
hour=KFET_WAKES_UP_AT) if scale_args is None:
return monday_morning 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(): def last_stats_manifest(scales_def=None, scale_args=None, scale_prefix=None,
now = timezone.now() **url_params):
morning = timezone.datetime(year=now.year, scales_def = [
month=now.month, ('Derniers mois', MonthScale, ),
day=now.day, ('Dernières semaines', WeekScale, ),
hour=KFET_WAKES_UP_AT) ('Derniers jours', DayScale, ),
return morning ]
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 # Étant donné un queryset d'operations
@ -100,3 +157,78 @@ def this_morning():
def tot_ventes(queryset): def tot_ventes(queryset):
res = queryset.aggregate(Sum('article_nb'))['article_nb__sum'] res = queryset.aggregate(Sum('article_nb'))['article_nb__sum']
return res and res or 0 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
]

View file

@ -13,12 +13,17 @@
{% if account.user == request.user %} {% 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/Chart.bundle.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/statistic.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/statistic.js' %}"></script>
<script>
jQuery(document).ready(function() { <script type="text/javascript">
var stat_last = new StatsGroup("{% url 'kfet.account.stat.last' trigramme=account.trigramme %}", $(document).ready(function() {
$("#stat_last")); var stat_last = new StatsGroup(
var stat_balance = new StatsGroup("{% url 'kfet.account.stat.balance' trigramme=account.trigramme %}", "{% url 'kfet.account.stat.operation.list' trigramme=account.trigramme %}",
$("#stat_balance")); $("#stat_last"),
);
var stat_balance = new StatsGroup(
"{% url 'kfet.account.stat.balance.list' trigramme=account.trigramme %}",
$("#stat_balance"),
);
}); });
</script> </script>
{% endif %} {% endif %}
@ -66,22 +71,22 @@
{% if account.user == request.user %} {% if account.user == request.user %}
<div class="content-right-block content-right-block-transparent"> <div class="content-right-block content-right-block-transparent">
<h2>Statistiques</h2> <h2>Statistiques</h2>
<div class="row"> <div class="row">
<div class="col-sm-12 nopadding"> <div class="col-sm-12 nopadding">
<div class="panel-md-margin"> <div class="panel-md-margin">
<h3>Ma balance</h3> <h3>Ma balance</h3>
<div id="stat_balance" class"stat-graph"></div> <div id="stat_balance"></div>
</div>
</div> </div>
</div><!-- /row --> </div>
<div class="row"> </div><!-- /row -->
<div class="col-sm-12 nopadding"> <div class="row">
<div class="panel-md-margin"> <div class="col-sm-12 nopadding">
<h3>Ma consommation</h3> <div class="panel-md-margin">
<div id="stat_last" class"stat-graph"></div> <h3>Ma consommation</h3>
</div> <div id="stat_last"></div>
</div> </div>
</div><!-- /row --> </div>
</div><!-- /row -->
</div> </div>
{% endif %} {% endif %}
<div class="content-right-block"> <div class="content-right-block">

View file

@ -1,5 +0,0 @@
{% if account.user == request.user %}
Mon compte
{% else %}
Informations du compte {{ account.trigramme }}
{% endif %}

View file

@ -1,6 +1,11 @@
{% extends 'kfet/base.html' %} {% extends 'kfet/base.html' %}
{% load staticfiles %} {% 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 title %}Informations sur l'article {{ article }}{% endblock %}
{% block content-header-title %}Article - {{ article.name }}{% endblock %} {% block content-header-title %}Article - {{ article.name }}{% endblock %}
@ -82,27 +87,26 @@
</div> </div>
<div class="content-right-block content-right-block-transparent"> <div class="content-right-block content-right-block-transparent">
<h2>Statistiques</h2> <h2>Statistiques</h2>
<div class="row"> <div class="row">
<div class="col-sm-12 nopadding"> <div class="col-sm-12 nopadding">
<div class="panel-md-margin"> <div class="panel-md-margin">
<h3>Ventes de {{ article.name }}</h3> <h3>Ventes de {{ article.name }}</h3>
<div id="stat_last"></div> <div id="stat_last"></div>
</div>
</div> </div>
</div><!-- /row --> </div>
</div><!-- /row -->
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %}
{% block extra_head %} <script type="text/javascript">
<script type="text/javascript" src="{% static 'kfet/js/Chart.bundle.js' %}"></script> $(document).ready(function() {
<script type="text/javascript" src="{% static 'kfet/js/statistic.js' %}"></script> var stat_last = new StatsGroup(
<script> "{% url 'kfet.article.stat.sales.list' article.id %}",
jQuery(document).ready(function() { $("#stat_last"),
var stat_last = new StatsGroup("{% url 'kfet.article.stat.last' article.id %}", );
$("#stat_last"));
}); });
</script> </script>
{% endblock %} {% endblock %}

View file

@ -1,5 +1,70 @@
# -*- coding: utf-8 -*- # -*- 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, "/")

View file

@ -69,28 +69,19 @@ urlpatterns = [
name='kfet.account.negative'), name='kfet.account.negative'),
# Account - Statistics # Account - Statistics
url('^accounts/(?P<trigramme>.{3})/stat/last/$', url('^accounts/(?P<trigramme>.{3})/stat/operations/list$',
views.AccountStatLastAll.as_view(), views.AccountStatOperationList.as_view(),
name = 'kfet.account.stat.last'), name='kfet.account.stat.operation.list'),
url('^accounts/(?P<trigramme>.{3})/stat/last/month/$', url('^accounts/(?P<trigramme>.{3})/stat/operations$',
views.AccountStatLastMonth.as_view(), views.AccountStatOperation.as_view(),
name = 'kfet.account.stat.last.month'), name='kfet.account.stat.operation'),
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/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
@ -149,20 +140,14 @@ urlpatterns = [
# Article - Update # Article - Update
url('^articles/(?P<pk>\d+)/edit$', url('^articles/(?P<pk>\d+)/edit$',
teamkfet_required(views.ArticleUpdate.as_view()), teamkfet_required(views.ArticleUpdate.as_view()),
name = 'kfet.article.update'), name='kfet.article.update'),
# Article - Statistics # Article - Statistics
url('^articles/(?P<pk>\d+)/stat/last/$', url(r'^articles/(?P<pk>\d+)/stat/sales/list$',
views.ArticleStatLastAll.as_view(), views.ArticleStatSalesList.as_view(),
name = 'kfet.article.stat.last'), name='kfet.article.stat.sales.list'),
url('^articles/(?P<pk>\d+)/stat/last/month/$', url(r'^articles/(?P<pk>\d+)/stat/sales$',
views.ArticleStatLastMonth.as_view(), views.ArticleStatSales.as_view(),
name = 'kfet.article.stat.last.month'), name='kfet.article.stat.sales'),
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'),
# ----- # -----
# K-Psul urls # K-Psul urls

View file

@ -1,12 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import ast
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
@ -46,10 +48,8 @@ from decimal import Decimal
import django_cas_ng import django_cas_ng
import heapq import heapq
import statistics import statistics
from .statistic import daynames, monthnames, weeknames, \ from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes
lastdays, lastweeks, lastmonths, \
this_morning, this_monday_morning, this_first_month_day, \
tot_ventes
class Home(TemplateView): class Home(TemplateView):
template_name = "kfet/home.html" template_name = "kfet/home.html"
@ -2029,87 +2029,54 @@ class JSONResponseMixin(object):
return context return context
class JSONDetailView(JSONResponseMixin, class JSONDetailView(JSONResponseMixin, BaseDetailView):
BaseDetailView): """Returns a DetailView that renders a JSON."""
"""
Returns a DetailView that renders a JSON
"""
def render_to_response(self, context): def render_to_response(self, context):
return self.render_to_json_response(context) return self.render_to_json_response(context)
class HybridDetailView(JSONResponseMixin,
SingleObjectTemplateResponseMixin, class PkUrlMixin(object):
BaseDetailView):
""" def get_object(self, *args, **kwargs):
Returns a DetailView as an html page except if a JSON file is requested get_by = self.kwargs.get(self.pk_url_kwarg)
by the GET method in which case it returns a JSON response. return get_object_or_404(self.model, **{self.pk_url_kwarg: get_by})
"""
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 HybridListView(JSONResponseMixin, class SingleResumeStat(JSONDetailView):
MultipleObjectTemplateResponseMixin, """Manifest for a kind of a stat about an object.
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)
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 = '' 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
@ -2124,87 +2091,84 @@ class ObjectResumeStat(JSONDetailView):
ID_PREFIX_ACC_BALANCE = "balance_acc" ID_PREFIX_ACC_BALANCE = "balance_acc"
# Un résumé de toutes les vues ArticleStatBalance class AccountStatBalanceList(PkUrlMixin, SingleResumeStat):
# REND DU JSON """Manifest for balance stats of an account."""
class AccountStatBalanceAll(ObjectResumeStat):
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):
""" """Datasets of balance of an account.
Returns a JSON containing the evolution a the personnal
balance of a trigramme between timezone.now() and `nb_days` Operations and Transfers are taken into account.
ago (specified to the view as an argument)
takes into account the Operations and the Transfers
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 last_days is not None:
begin_date = timezone.datetime(year=1980, month=1, day=1) end_date = timezone.now()
anytime = True begin_date = end_date - timezone.timedelta(days=last_days)
else:
begin_date = this_morning() \ # prepare querysets
- timezone.timedelta(days=int(nb_date))
anytime = False
# 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,
@ -2214,76 +2178,86 @@ class AccountStatBalance(JSONDetailView):
# sera mis à jour lors d'une # sera mis à jour lors d'une
# autre passe) # 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(), 'at': ope_grp.at.isoformat(),
'amout': 0, 'amount': ope_grp.amount,
'label': "actuel", 'label': str(ope_grp),
'balance': 0, 'balance': 0,
} } for ope_grp in opegroups
] + [
{
'at': op.at.isoformat(),
'amount': op.amount,
'label': str(op),
'balance': 0,
} for op in opgroups
] + [ ] + [
{ {
'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) if len(actions) > 1:
actions[0]['balance'] = account.balance actions = sorted(actions, key=lambda k: k['at'], reverse=True)
for i in range(len(actions)-1): actions[0]['balance'] = account.balance
actions[i+1]['balance'] = actions[i]['balance'] \ for i in range(len(actions)-1):
- actions[i+1]['amount'] actions[i+1]['balance'] = \
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'] if len(changes) > 0:
context['max_date'] = changes[0]['at'] context['min_date'] = changes[-1]['at']
context['max_date'] = changes[0]['at']
# TODO: offset # TODO: offset
return context 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) @method_decorator(login_required)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
return super(AccountStatBalance, self).dispatch(*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" ID_PREFIX_ACC_LAST_MONTHS = "last_months_acc"
# Un résumé de toutes les vues ArticleStatLast class AccountStatOperationList(PkUrlMixin, SingleResumeStat):
# NE REND PAS DE JSON """Manifest for operations stats of an account."""
class AccountStatLastAll(ObjectResumeStat):
model = Account model = Account
context_object_name = 'account' context_object_name = 'account'
trigramme_url_kwarg = 'trigramme' pk_url_kwarg = 'trigramme'
id_prefix = ID_PREFIX_ACC_LAST id_prefix = ID_PREFIX_ACC_LAST
nb_stat = 3
nb_default = 2 nb_default = 2
stat_labels = ["Derniers mois", "Dernières semaines", "Derniers jours"] stats = last_stats_manifest(types=[Operation.PURCHASE])
stat_urls = ['kfet.account.stat.last.month', url_stat = 'kfet.account.stat.operation'
'kfet.account.stat.last.week',
'kfet.account.stat.last.day']
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}
@method_decorator(login_required) @method_decorator(login_required)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
return super(AccountStatLastAll, self).dispatch(*args, **kwargs) return super().dispatch(*args, **kwargs)
class AccountStatLast(JSONDetailView): class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView):
""" """Datasets of operations of an account."""
Returns a JSON containing the evolution a the personnal
consommation of a trigramme at the diffent dates specified
"""
model = Account model = Account
trigramme_url_kwarg = 'trigramme' pk_url_kwarg = 'trigramme'
context_object_name = 'account' context_object_name = 'account'
end_date = timezone.now()
id_prefix = "" id_prefix = ""
# doit rendre un dictionnaire des dates def get_operations(self, scale, types=None):
# 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
# On selectionne les opérations qui correspondent # On selectionne les opérations qui correspondent
# à l'article en question et qui ne sont pas annulées # à l'article en question et qui ne sont pas annulées
# puis on choisi pour chaques intervalle les opérations # puis on choisi pour chaques intervalle les opérations
# effectuées dans ces intervalles de temps # effectuées dans ces intervalles de temps
all_operations = (Operation.objects all_operations = (Operation.objects
.filter(type='purchase')
.filter(group__on_acc=self.object) .filter(group__on_acc=self.object)
.filter(canceled_at=None) .filter(canceled_at=None)
) )
operations = {} if types is not None:
for i in dates: all_operations = all_operations.filter(type__in=types)
operations[i] = (all_operations chunks = self.chunkify_qs(all_operations, scale, field='group__at')
.filter(group__at__gte=extended_dates[i]) return chunks
.filter(group__at__lte=extended_dates[i+1])
)
return operations
def get_context_data(self, **kwargs): def get_context_data(self, *args, **kwargs):
context = {} old_ctx = super().get_context_data(*args, **kwargs)
nb_ventes = {} context = {'labels': old_ctx['labels']}
# On récupère les labels des dates scale = self.scale
context['labels'] = self.get_labels().copy()
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 # On compte les opérations
operations = self.sort_operations() nb_ventes = []
for i in operations: for chunk in operations:
nb_ventes[i] = tot_ventes(operations[i]) nb_ventes.append(tot_ventes(chunk))
context['charts'] = [ { "color": "rgb(255, 99, 132)",
"label": "NB items achetés", context['charts'] = [{"color": "rgb(255, 99, 132)",
"values": nb_ventes } ] "label": "NB items achetés",
"values": nb_ventes}]
return context 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) @method_decorator(login_required)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
return super(AccountStatLast, self).dispatch(*args, **kwargs) return super().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)
# ------------------------ # ------------------------
@ -2443,143 +2354,64 @@ ID_PREFIX_ART_LAST_WEEKS = "last_weeks_art"
ID_PREFIX_ART_LAST_MONTHS = "last_months_art" ID_PREFIX_ART_LAST_MONTHS = "last_months_art"
# Un résumé de toutes les vues ArticleStatLast class ArticleStatSalesList(SingleResumeStat):
# NE REND PAS DE JSON """Manifest for sales stats of an article."""
class ArticleStatLastAll(ObjectResumeStat):
model = Article model = Article
context_object_name = 'article' context_object_name = 'article'
id_prefix = ID_PREFIX_ART_LAST id_prefix = ID_PREFIX_ART_LAST
nb_stat = 3
nb_default = 2 nb_default = 2
stat_labels = ["Derniers mois", "Dernières semaines", "Derniers jours"] url_stat = 'kfet.article.stat.sales'
stat_urls = ['kfet.article.stat.last.month', stats = last_stats_manifest()
'kfet.article.stat.last.week',
'kfet.article.stat.last.day']
@method_decorator(login_required) @method_decorator(teamkfet_required)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
return super(ArticleStatLastAll, self).dispatch(*args, **kwargs) return super().dispatch(*args, **kwargs)
class ArticleStatLast(JSONDetailView): class ArticleStatSales(ScaleMixin, JSONDetailView):
""" """Datasets of sales of an article."""
Returns a JSON containing the consommation
of an article at the diffent dates precised
"""
model = Article model = Article
context_object_name = 'article' context_object_name = 'article'
end_date = timezone.now()
id_prefix = ""
def render_to_response(self, context): def get_context_data(self, *args, **kwargs):
# Look for a 'format=json' GET argument old_ctx = super().get_context_data(*args, **kwargs)
if self.request.GET.get('format') == 'json': context = {'labels': old_ctx['labels']}
return self.render_to_json_response(context) scale = self.scale
else:
return super(ArticleStatLast, self).render_to_response(context)
# 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 # On selectionne les opérations qui correspondent
# à l'article en question et qui ne sont pas annulées # à l'article en question et qui ne sont pas annulées
# puis on choisi pour chaques intervalle les opérations # puis on choisi pour chaques intervalle les opérations
# effectuées dans ces intervalles de temps # effectuées dans ces intervalles de temps
all_operations = (Operation.objects all_operations = (
.filter(type='purchase') Operation.objects
.filter(article=self.object) .filter(type=Operation.PURCHASE,
.filter(canceled_at=None) article=self.object,
) canceled_at=None,
operations = {} )
for i in dates: )
operations[i] = (all_operations chunks = self.chunkify_qs(all_operations, scale, field='group__at')
.filter(group__at__gte=extended_dates[i])
.filter(group__at__lte=extended_dates[i+1])
)
# On compte les opérations # On compte les opérations
nb_ventes = {} nb_ventes = []
nb_accounts = {} nb_accounts = []
nb_liq = {} nb_liq = []
for i in operations: for qs in chunks:
nb_ventes[i] = tot_ventes(operations[i]) nb_ventes.append(
nb_liq[i] = tot_ventes( tot_ventes(qs))
operations[i] nb_liq.append(
.filter(group__on_acc__trigramme='LIQ') tot_ventes(qs.filter(group__on_acc__trigramme='LIQ')))
) nb_accounts.append(
nb_accounts[i] = tot_ventes( tot_ventes(qs.exclude(group__on_acc__trigramme='LIQ')))
operations[i] context['charts'] = [{"color": "rgb(255, 99, 132)",
.exclude(group__on_acc__trigramme='LIQ') "label": "Toutes consommations",
) "values": nb_ventes},
context['charts'] = [ { "color": "rgb(255, 99, 132)", {"color": "rgb(54, 162, 235)",
"label": "Toutes consommations", "label": "LIQ",
"values": nb_ventes }, "values": nb_liq},
{ "color": "rgb(54, 162, 235)", {"color": "rgb(255, 205, 86)",
"label": "LIQ", "label": "Comptes K-Fêt",
"values": nb_liq }, "values": nb_accounts}]
{ "color": "rgb(255, 205, 86)",
"label": "Comptes K-Fêt",
"values": nb_accounts } ]
return context return context
@method_decorator(login_required) @method_decorator(teamkfet_required)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
return super(ArticleStatLast, self).dispatch(*args, **kwargs) return super().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)

View file

@ -20,3 +20,4 @@ django-widget-tweaks==1.4.1
git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_custommail git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_custommail
ldap3 ldap3
git+https://github.com/Aureplop/channels.git#egg=channels git+https://github.com/Aureplop/channels.git#egg=channels
python-dateutil