Merge branch 'master' into aureplop/kfet_config

This commit is contained in:
Aurélien Delobelle 2017-04-08 18:36:28 +02:00
commit 784ba9bd10
29 changed files with 1331 additions and 833 deletions

View file

@ -18,7 +18,10 @@ class Tirage(models.Model):
fermeture = models.DateTimeField("Date et heure de fermerture du tirage") fermeture = models.DateTimeField("Date et heure de fermerture du tirage")
tokens = models.TextField("Graine(s) du tirage", blank=True) tokens = models.TextField("Graine(s) du tirage", blank=True)
active = models.BooleanField("Tirage actif", default=False) active = models.BooleanField("Tirage actif", default=False)
appear_catalogue = models.BooleanField("Tirage à afficher dans le catalogue", default=False) appear_catalogue = models.BooleanField(
"Tirage à afficher dans le catalogue",
default=False
)
enable_do_tirage = models.BooleanField("Le tirage peut être lancé", enable_do_tirage = models.BooleanField("Le tirage peut être lancé",
default=False) default=False)

View file

@ -1,22 +1,79 @@
# -*- coding: utf-8 -*- import json
"""
This file demonstrates writing tests using the unittest module. These will pass
when you run "manage.py test".
Replace this with more appropriate tests for your application. from django.test import TestCase, Client
""" from django.utils import timezone
from __future__ import division from .models import Tirage, Spectacle, Salle, CategorieSpectacle
from __future__ import print_function
from __future__ import unicode_literals
from django.test import TestCase class TestBdAViews(TestCase):
def setUp(self):
self.tirage = Tirage.objects.create(
title="Test tirage",
appear_catalogue=True,
ouverture=timezone.now(),
fermeture=timezone.now(),
)
self.category = CategorieSpectacle.objects.create(name="Category")
self.location = Salle.objects.create(name="here")
Spectacle.objects.bulk_create([
Spectacle(
title="foo", date=timezone.now(), location=self.location,
price=0, slots=42, tirage=self.tirage, listing=False,
category=self.category
),
Spectacle(
title="bar", date=timezone.now(), location=self.location,
price=1, slots=142, tirage=self.tirage, listing=False,
category=self.category
),
Spectacle(
title="baz", date=timezone.now(), location=self.location,
price=2, slots=242, tirage=self.tirage, listing=False,
category=self.category
),
])
def test_catalogue(self):
"""Test the catalogue JSON API"""
client = Client()
class SimpleTest(TestCase): # The `list` hooh
def test_basic_addition(self): resp = client.get("/bda/catalogue/list")
""" self.assertJSONEqual(
Tests that 1 + 1 always equals 2. resp.content.decode("utf-8"),
""" [{"id": self.tirage.id, "title": self.tirage.title}]
self.assertEqual(1 + 1, 2) )
# The `details` hook
resp = client.get(
"/bda/catalogue/details?id={}".format(self.tirage.id)
)
self.assertJSONEqual(
resp.content.decode("utf-8"),
{
"categories": [{
"id": self.category.id,
"name": self.category.name
}],
"locations": [{
"id": self.location.id,
"name": self.location.name
}],
}
)
# The `descriptions` hook
resp = client.get(
"/bda/catalogue/descriptions?id={}".format(self.tirage.id)
)
raw = resp.content.decode("utf-8")
try:
results = json.loads(raw)
except ValueError:
self.fail("Not valid JSON: {}".format(raw))
self.assertEqual(len(results), 3)
self.assertEqual(
{(s["title"], s["price"], s["slots"]) for s in results},
{("foo", 0, 42), ("bar", 1, 142), ("baz", 2, 242)}
)

View file

@ -22,7 +22,6 @@ from django.core.urlresolvers import reverse
from django.conf import settings from django.conf import settings
from django.utils import timezone, formats from django.utils import timezone, formats
from django.views.generic.list import ListView from django.views.generic.list import ListView
from django.core.exceptions import ObjectDoesNotExist
from gestioncof.decorators import cof_required, buro_required from gestioncof.decorators import cof_required, buro_required
from bda.models import ( from bda.models import (
Spectacle, Participant, ChoixSpectacle, Attribution, Tirage, Spectacle, Participant, ChoixSpectacle, Attribution, Tirage,
@ -657,29 +656,35 @@ def catalogue(request, request_type):
if request_type == "list": if request_type == "list":
# Dans ce cas on retourne la liste des tirages et de leur id en JSON # Dans ce cas on retourne la liste des tirages et de leur id en JSON
data_return = list( data_return = list(
Tirage.objects.filter(appear_catalogue=True).values('id', 'title')) Tirage.objects.filter(appear_catalogue=True).values('id', 'title')
)
return JsonResponse(data_return, safe=False) return JsonResponse(data_return, safe=False)
if request_type == "details": if request_type == "details":
# Dans ce cas on retourne une liste des catégories et des salles # Dans ce cas on retourne une liste des catégories et des salles
tirage_id = request.GET.get('id', '') tirage_id = request.GET.get('id', None)
try: if tirage_id is None:
tirage = Tirage.objects.get(id=tirage_id)
except ObjectDoesNotExist:
return HttpResponseBadRequest( return HttpResponseBadRequest(
"Aucun tirage correspondant à l'id " "Missing GET parameter: id <int>"
+ tirage_id) )
try:
tirage = get_object_or_404(Tirage, id=int(tirage_id))
except ValueError: except ValueError:
return HttpResponseBadRequest( return HttpResponseBadRequest(
"Mauvais format d'identifiant : " "Bad format: int expected for `id`"
+ tirage_id) )
shows = tirage.spectacle_set.values_list("id", flat=True)
categories = list( categories = list(
CategorieSpectacle.objects.filter( CategorieSpectacle.objects
spectacle__in=tirage.spectacle_set.all()) .filter(spectacle__in=shows)
.distinct().values('id', 'name')) .distinct()
.values('id', 'name')
)
locations = list( locations = list(
Salle.objects.filter( Salle.objects
spectacle__in=tirage.spectacle_set.all()) .filter(spectacle__in=shows)
.distinct().values('id', 'name')) .distinct()
.values('id', 'name')
)
data_return = {'categories': categories, 'locations': locations} data_return = {'categories': categories, 'locations': locations}
return JsonResponse(data_return, safe=False) return JsonResponse(data_return, safe=False)
if request_type == "descriptions": if request_type == "descriptions":
@ -687,33 +692,35 @@ def catalogue(request, request_type):
# à la salle spécifiées # à la salle spécifiées
tirage_id = request.GET.get('id', '') tirage_id = request.GET.get('id', '')
categories = request.GET.get('category', '[0]') categories = request.GET.get('category', '[]')
locations = request.GET.get('location', '[0]') locations = request.GET.get('location', '[]')
try: try:
category_id = json.loads(categories) tirage_id = int(tirage_id)
location_id = json.loads(locations) categories_id = json.loads(categories)
tirage = Tirage.objects.get(id=tirage_id) locations_id = json.loads(locations)
# Integers expected
shows_qs = tirage.spectacle_set if not all(isinstance(id, int) for id in categories_id):
if not(0 in category_id): raise ValueError
shows_qs = shows_qs.filter( if not all(isinstance(id, int) for id in locations_id):
category__id__in=category_id) raise ValueError
if not(0 in location_id):
shows_qs = shows_qs.filter(
location__id__in=location_id)
except ObjectDoesNotExist:
return HttpResponseBadRequest(
"Impossible de trouver des résultats correspondant "
"à ces caractéristiques : "
+ "id = " + tirage_id
+ ", catégories = " + categories
+ ", salles = " + locations)
except ValueError: # Contient JSONDecodeError except ValueError: # Contient JSONDecodeError
return HttpResponseBadRequest( return HttpResponseBadRequest(
"Impossible de parser les paramètres donnés : " "Parse error, please ensure the GET parameters have the "
+ "id = " + request.GET.get('id', '') "following types:\n"
+ ", catégories = " + request.GET.get('category', '[0]') "id: int, category: [int], location: [int]\n"
+ ", salles = " + request.GET.get('location', '[0]')) "Data received:\n"
"id = {}, category = {}, locations = {}"
.format(request.GET.get('id', ''),
request.GET.get('category', '[]'),
request.GET.get('location', '[]'))
)
tirage = get_object_or_404(Tirage, id=tirage_id)
shows_qs = tirage.spectacle_set
if categories_id:
shows_qs = shows_qs.filter(category__id__in=categories_id)
if locations_id:
shows_qs = shows_qs.filter(location__id__in=locations_id)
# On convertit les descriptions à envoyer en une liste facilement # On convertit les descriptions à envoyer en une liste facilement
# JSONifiable (il devrait y avoir un moyen plus efficace en # JSONifiable (il devrait y avoir un moyen plus efficace en

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

@ -239,6 +239,16 @@ class CheckoutStatementUpdateForm(forms.ModelForm):
model = CheckoutStatement model = CheckoutStatement
exclude = ['by', 'at', 'checkout', 'amount_error', 'amount_taken'] exclude = ['by', 'at', 'checkout', 'amount_error', 'amount_taken']
# -----
# Category
# -----
class CategoryForm(forms.ModelForm):
class Meta:
model = ArticleCategory
fields = ['name', 'has_addcost']
# ----- # -----
# Article forms # Article forms
# ----- # -----
@ -471,7 +481,7 @@ class InventoryArticleForm(forms.Form):
queryset = Article.objects.all(), queryset = Article.objects.all(),
widget = forms.HiddenInput(), widget = forms.HiddenInput(),
) )
stock_new = forms.IntegerField(required = False) stock_new = forms.IntegerField(required=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(InventoryArticleForm, self).__init__(*args, **kwargs) super(InventoryArticleForm, self).__init__(*args, **kwargs)
@ -480,6 +490,7 @@ class InventoryArticleForm(forms.Form):
self.stock_old = kwargs['initial']['stock_old'] self.stock_old = kwargs['initial']['stock_old']
self.category = kwargs['initial']['category'] self.category = kwargs['initial']['category']
self.category_name = kwargs['initial']['category__name'] self.category_name = kwargs['initial']['category__name']
self.box_capacity = kwargs['initial']['box_capacity']
# ----- # -----
# Order forms # Order forms

View file

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kfet', '0051_verbose_names'),
]
operations = [
migrations.AddField(
model_name='articlecategory',
name='has_addcost',
field=models.BooleanField(default=True, help_text="Si oui et qu'une majoration est active, celle-ci sera appliquée aux articles de cette catégorie.", verbose_name='majorée'),
),
migrations.AlterField(
model_name='articlecategory',
name='name',
field=models.CharField(max_length=45, verbose_name='nom'),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('kfet', '0052_category_addcost'),
]
operations = [
migrations.AlterField(
model_name='account',
name='created_at',
field=models.DateTimeField(default=django.utils.timezone.now),
),
]

View file

@ -37,7 +37,7 @@ class Account(models.Model):
max_digits = 6, decimal_places = 2, max_digits = 6, decimal_places = 2,
default = 0) default = 0)
is_frozen = models.BooleanField("est gelé", default = False) is_frozen = models.BooleanField("est gelé", default = False)
created_at = models.DateTimeField(auto_now_add = True, null = True) created_at = models.DateTimeField(default=timezone.now)
# Optional # Optional
PROMO_CHOICES = [(r,r) for r in range(1980, date.today().year+1)] PROMO_CHOICES = [(r,r) for r in range(1980, date.today().year+1)]
promo = models.IntegerField( promo = models.IntegerField(
@ -334,13 +334,20 @@ class CheckoutStatement(models.Model):
balance=F('balance') - last_statement.balance_new + self.balance_new) balance=F('balance') - last_statement.balance_new + self.balance_new)
super(CheckoutStatement, self).save(*args, **kwargs) super(CheckoutStatement, self).save(*args, **kwargs)
@python_2_unicode_compatible @python_2_unicode_compatible
class ArticleCategory(models.Model): class ArticleCategory(models.Model):
name = models.CharField(max_length = 45) name = models.CharField("nom", max_length=45)
has_addcost = models.BooleanField("majorée", default=True,
help_text="Si oui et qu'une majoration "
"est active, celle-ci sera "
"appliquée aux articles de "
"cette catégorie.")
def __str__(self): def __str__(self):
return self.name return self.name
@python_2_unicode_compatible @python_2_unicode_compatible
class Article(models.Model): class Article(models.Model):
name = models.CharField("nom", max_length = 45) name = models.CharField("nom", max_length = 45)
@ -487,24 +494,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

@ -32,6 +32,7 @@ textarea {
.table { .table {
margin-bottom:0; margin-bottom:0;
border-bottom:1px solid #ddd;
} }
.table { .table {
@ -85,6 +86,16 @@ textarea {
color:#FFF; color:#FFF;
} }
.buttons .nav-pills > li > a {
border-radius:0;
border:1px solid rgba(200,16,46,0.9);
}
.buttons .nav-pills > li.active > a {
background-color:rgba(200,16,46,0.9);
background-clip:padding-box;
}
.row-page-header { .row-page-header {
background-color:rgba(200,16,46,1); background-color:rgba(200,16,46,1);
color:#FFF; color:#FFF;
@ -105,6 +116,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;
@ -230,6 +242,9 @@ textarea {
height:28px; height:28px;
margin:3px 0px; margin:3px 0px;
} }
.content-center .auth-form {
margin:15px;
}
/* /*
* Pages formulaires seuls * Pages formulaires seuls
@ -549,3 +564,18 @@ thead .tooltip {
.help-block { .help-block {
padding-top: 15px; padding-top: 15px;
} }
/* Inventaires */
.inventory_modified {
background:rgba(236,100,0,0.15);
}
.stock_diff {
padding-left: 5px;
color:#C8102E;
}
.inventory_update {
display:none;
}

View file

@ -28,6 +28,7 @@
.jconfirm .jconfirm-box .content { .jconfirm .jconfirm-box .content {
border-bottom:1px solid #ddd; border-bottom:1px solid #ddd;
padding:5px 10px;
} }
.jconfirm .jconfirm-box input { .jconfirm .jconfirm-box input {

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 %}
@ -51,45 +56,40 @@
<div class="col-sm-8 col-md-9 col-content-right"> <div class="col-sm-8 col-md-9 col-content-right">
{% include "kfet/base_messages.html" %} {% include "kfet/base_messages.html" %}
<div class="content-right"> <div class="content-right">
{% if addcosts %}
<div class="content-right-block">
<h2>Gagné des majorations</h2>
<div>
<ul>
{% for addcost in addcosts %}
<li>{{ addcost.date|date:'l j F' }}: +{{ addcost.sum_addcosts }}€</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% 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>
</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>
{% endif %}
<div class="content-right-block"> <div class="content-right-block">
<h2>Historique</h2> <div class="col-sm-12 nopadding">
<div id="history"> {% if account.user == request.user %}
</div> <div class='tab-content'>
</div> <div class="tab-pane fade in active" id="tab_stats">
</div> <h2>Statistiques</h2>
<div class="panel-md-margin">
<h3>Ma balance</h3>
<div id="stat_balance"></div>
<h3>Ma consommation</h3>
<div id="stat_last"></div>
</div>
</div>
<div class="tab-pane fade" id="tab_history">
{% endif %}
{% if addcosts %}
<h2>Gagné des majorations</h2>
<div>
<ul>
{% for addcost in addcosts %}
<li>{{ addcost.date|date:'l j F' }}: +{{ addcost.sum_addcosts }}€</li>
{% endfor %}
</ul>
</div>
{% endif %}
<h2>Historique</h2>
<div id="history"></div>
{% if account.user == request.user %}
</div>
</div><!-- tab-content -->
{% endif %}
</div><!-- col-sm-12 -->
</div><!-- content-right-block -->
</div><!-- content-right-->
</div> </div>
</div> </div>

View file

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

View file

@ -16,6 +16,9 @@
<a class="btn btn-primary btn-lg" href="{% url 'kfet.article.create' %}"> <a class="btn btn-primary btn-lg" href="{% url 'kfet.article.create' %}">
Nouvel article Nouvel article
</a> </a>
<a class="btn btn-primary btn-lg" href="{% url 'kfet.category' %}">
Catégories
</a>
</div> </div>
</div> </div>
</div> </div>

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

@ -12,7 +12,7 @@
<div class="row form-only"> <div class="row form-only">
<div class="col-sm-12 col-md-8 col-md-offset-2"> <div class="col-sm-12 col-md-8 col-md-offset-2">
<div class="content-form"> <div class="content-form">
<form submit="" method="post" class="form-horizontal"> <form action="" method="post" class="form-horizontal">
{% csrf_token %} {% csrf_token %}
{% include 'kfet/form_snippet.html' with form=form %} {% include 'kfet/form_snippet.html' with form=form %}
{% if not perms.kfet.change_article %} {% if not perms.kfet.change_article %}

View file

@ -0,0 +1,53 @@
{% extends 'kfet/base.html' %}
{% block title %}Categories d'articles{% endblock %}
{% block content-header-title %}Categories d'articles{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-4 col-md-3 col-content-left">
<div class="content-left">
<div class="content-left-top">
<div class="line line-big">{{ categories|length }}</div>
<div class="line line-bigsub">catégorie{{ categories|length|pluralize }}</div>
</div>
</div>
</div>
<div class="col-sm-8 col-md-9 col-content-right">
{% include 'kfet/base_messages.html' %}
<div class="content-right">
<div class="content-right-block">
<h2>Liste des catégories</h2>
<div class="table-responsive">
<table class="table table-condensed">
<thead>
<tr>
<td></td>
<td>Nom</td>
<td class="text-right">Nombre d'articles</td>
<td class="text-right">Peut être majorée</td>
</tr>
</thead>
<tbody>
{% for category in categories %}
<tr>
<td class="text-center">
<a href="{% url 'kfet.category.update' category.pk %}">
<span class="glyphicon glyphicon-cog"></span>
</a>
</td>
<td>{{ category.name }}</td>
<td class="text-right">{{ category.articles.all|length }}</td>
<td class="text-right">{{ category.has_addcost | yesno:"Oui,Non"}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,25 @@
{% extends 'kfet/base.html' %}
{% block title %}Édition de la catégorie {{ category.name }}{% endblock %}
{% block content-header-title %}Catégorie {{ category.name }} - Édition{% endblock %}
{% block content %}
{% include "kfet/base_messages.html" %}
<div class="row form-only">
<div class="col-sm-12 col-md-8 col-md-offset-2">
<div class="content-form">
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% include 'kfet/form_snippet.html' with form=form %}
{% if not perms.kfet.edit_articlecategory %}
{% include 'kfet/form_authentication_snippet.html' %}
{% endif %}
{% include 'kfet/form_submit_snippet.html' with value="Enregistrer"%}
<form>
</div>
</div>
</div>
{% endblock %}

View file

@ -20,40 +20,42 @@
<div class="content-right"> <div class="content-right">
<div class="content-right-block"> <div class="content-right-block">
<h2>Carte</h2> <h2>Carte</h2>
<div class="column-row"> <div class="column-row">
<div class="column-sm-1 column-md-2 column-lg-3"> <div class="column-sm-1 column-md-2 column-lg-3">
<div class="unbreakable carte-inverted"> <div class="unbreakable carte-inverted">
<h3>Pressions du moment</h3> {% if pressions %}
<ul class="carte"> <h3>Pressions du moment</h3>
{% for article in pressions %} <ul class="carte">
<li class="carte-line"> {% for article in pressions %}
<div class="filler"></div> <li class="carte-line">
<span class="carte-label">{{ article.name }}</span> <div class="filler"></div>
<span class="carte-ukf">{{ article.price | ukf:False}} UKF</span> <span class="carte-label">{{ article.name }}</span>
</li> <span class="carte-ukf">{{ article.price | ukf:False}} UKF</span>
{% endfor %} </li>
</ul>
</div><!-- endblock unbreakable -->
{% for article in articles %}
{% ifchanged article.category %}
{% if not forloop.first %}
</ul>
</div><!-- endblock unbreakable -->
{% endif %}
<div class="unbreakable">
<h3>{{ article.category.name }}</h3>
<ul class="carte">
{% endifchanged %}
<li class="carte-line">
<div class="filler"></div>
<span class="carte-label">{{ article.name }}</span>
<span class="carte-ukf">{{ article.price | ukf:False}} UKF</span>
</li>
{% if foorloop.last %}
</ul>
</div><!-- endblock unbreakable -->
{% endif %}
{% endfor %} {% endfor %}
</ul>
{% endif %}
</div><!-- endblock unbreakable -->
{% for article in articles %}
{% ifchanged article.category %}
{% if not forloop.first %}
</ul>
</div><!-- endblock unbreakable -->
{% endif %}
<div class="unbreakable">
<h3>{{ article.category.name }}</h3>
<ul class="carte">
{% endifchanged %}
<li class="carte-line">
<div class="filler"></div>
<span class="carte-label">{{ article.name }}</span>
<span class="carte-ukf">{{ article.price | ukf:False}} UKF</span>
</li>
{% if forloop.last %}
</ul>
</div><!-- endblock unbreakable -->
{% endif %}
{% endfor %}
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,4 +1,11 @@
{% extends 'kfet/base.html' %} {% extends 'kfet/base.html' %}
{% load staticfiles %}
{% load widget_tweaks %}
{% block extra_head %}
<script type="text/javascript" src="{% static 'kfet/js/reconnecting-websocket.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/jquery-confirm.js' %}"></script>
{% endblock %}
{% block title %}Nouvel inventaire{% endblock %} {% block title %}Nouvel inventaire{% endblock %}
{% block content-header-title %}Nouvel inventaire{% endblock %} {% block content-header-title %}Nouvel inventaire{% endblock %}
@ -6,38 +13,194 @@
{% block content %} {% block content %}
{% include 'kfet/base_messages.html' %} {% include 'kfet/base_messages.html' %}
<div class="content-center">
<form action="" method="post"> <div>
<table> <form id='inventoryform' action="" method="post">
<thead> <table class="table text-center">
<tr> <thead>
<td>Article</td>
<td>Théo.</td>
<td>Réel</td>
</tr>
</thead>
<tbody>
{% for form in formset %}
{% ifchanged form.category %}
<tr> <tr>
<td colspan="3">{{ form.category_name }}</td> <td>Article</td>
<td>Quantité par caisse</td>
<td>Stock Théorique</td>
<td>Caisses en réserve</td>
<td>Caisses en arrière</td>
<td>Vrac</td>
<td>Stock total</td>
<td>Compte terminé</td>
</tr> </tr>
{% endifchanged %} </thead>
<tr> <tbody>
{{ form.article }} {% for form in formset %}
<td>{{ form.name }}</td> {% ifchanged form.category %}
<td>{{ form.stock_old }}</td> <tr class='section'>
<td>{{ form.stock_new }}</td> <td>{{ form.category_name }}</td>
</tr> <td colspan="7"></td>
{% endfor %} </tr>
</tbody> {% endifchanged %}
</table> <tr>
{% if not perms.kfet.add_inventory %} {{ form.article }}
<input type="password" name="KFETPASSWORD"> <td class='name'>{{ form.name }}</td>
{% endif %} <td class='box_capacity'>{{ form.box_capacity }}</td>
{% csrf_token %} <td><span class='current_stock'>{{ form.stock_old }}</span><span class='stock_diff'></span></td>
{{ formset.management_form }} <td class='box_cellar'>
<input type="submit" value="Enregistrer" class="btn btn-primary btn-lg"> <div class='col-md-2'></div>
</form> <div class='col-md-8'>
<input type='number' class='form-control' step='1'>
</div>
</td>
<td class='box_bar'>
<div class='col-md-offset-2 col-md-8'><input type='number' class='form-control' step='1'></div>
</td>
<td class='misc'>
<div class='col-md-offset-2 col-md-8'><input type='number' class='form-control' step='1'></div>
</td>
<td class='stock_new'>
<div class='col-md-offset-2 col-md-8'>{{ form.stock_new | attr:"readonly"| add_class:"form-control" }}</div>
<div class='col-md-2 inventory_update'><button type='button' class='btn-sm btn-primary'>MàJ</button></div>
</td>
<td class='finished'><input type='checkbox' class='form_control'></td>
</tr>
{% endfor %}
</tbody>
</table>
{{ formset.management_form }}
{% if not perms.kfet.add_inventory %}
<div class='auth-form form-horizontal'>
{% include "kfet/form_authentication_snippet.html" %}
</div>
{% endif %}
<input type="submit" value="Enregistrer" class="btn btn-primary btn-lg btn-block">
{% csrf_token %}
</form>
</div>
</div>
<script type="text/javascript">
$(document).ready(function() {
'use strict';
var conflicts = new Set();
/**
* Autofill new stock from other inputs
*/
$('input[type="number"]').on('input', function() {
var $line = $(this).closest('tr');
var box_capacity = +$line.find('.box_capacity').text();
var box_cellar = $line.find('.box_cellar input').val();
var box_bar = $line.find('.box_bar input').val();
var misc = $line.find('.misc input').val();
if (box_cellar || box_bar || misc)
$line.find('.stock_new input').val(
box_capacity*((+box_cellar) +(+box_bar))+(+misc));
else
$line.find('.stock_new input').val('');
});
/*
* Remove warning and update stock
*/
function update_stock($line, update_count) {
$line.removeClass('inventory_modified');
$line.find('.inventory_update').hide();
var old_stock = +$line.find('.current_stock').text()
var stock_diff = +$line.find('.stock_diff').text();
$line.find('.current_stock').text(old_stock + stock_diff);
$line.find('.stock_diff').text('');
if ($line.find('.stock_new input').val() && update_count) {
var old_misc = +$line.find('.misc input').val();
$line.find('.misc input').val(old_misc + stock_diff)
.trigger('input');
}
var id = $line.find('input[type="hidden"]').val();
conflicts.delete(parseInt(id));
}
$('.finished input').change(function() {
var $line = $(this).closest('tr');
update_stock($line, false);
});
$('.inventory_update button').click(function() {
var $line = $(this).closest('tr');
update_stock($line, true);
});
/**
* Websocket
*/
OperationWebSocket.add_handler(function(data) {
for (let article of data['articles']) {
var $line = $('input[value="'+article.id+'"]').parent();
if ($line.find('.finished input').is(":checked")) {
conflicts.add(article.id);
//Display warning
$line.addClass('inventory_modified');
//Realigning input and displaying update button
$line.find('.inventory_update').show();
//Displaying stock changes
var stock = $line.find('.current_stock').text();
$line.find('.stock_diff').text(article.stock - stock);
} else {
// If we haven't counted the article yet, we simply update the expected stock
$line.find('.current_stock').text(article.stock);
}
}
});
$('input[type="submit"]').on("click", function(e) {
e.preventDefault();
if (conflicts.size) {
content = '';
content += "Conflits possibles :"
content += '<ul>';
for (let id of conflicts) {
var $line = $('input[value="'+id+'"]').closest('tr');
var name = $line.find('.name').text();
var stock_diff = $line.find('.stock_diff').text();
content += '<li>'+name+' ('+stock_diff+')</li>';
}
content += '</ul>'
} else {
// Prevent erroneous enter key confirmations
// Kinda complicated to filter if click or enter key...
content="Voulez-vous confirmer l'inventaire ?";
}
$.confirm({
title: "Confirmer l'inventaire",
content: content,
backgroundDismiss: true,
animation: 'top',
closeAnimation: 'bottom',
keyboardEnabled: true,
confirm: function() {
$('#inventoryform').submit();
},
onOpen: function() {
var that = this;
this.$content.find('input').on('keydown', function(e) {
if (e.keyCode == 13)
that.$confirmButton.click();
});
},
});
});
});
</script>
{% endblock %} {% endblock %}

View file

@ -647,7 +647,7 @@ $(document).ready(function() {
}); });
$after.after(article_html); $after.after(article_html);
// Pour l'autocomplétion // Pour l'autocomplétion
articlesList.push([article['name'],article['id'],article['category_id'],article['price'], article['stock']]); articlesList.push([article['name'],article['id'],article['category_id'],article['price'], article['stock'],article['category__has_addcost']]);
} }
function getArticles() { function getArticles() {
@ -831,12 +831,15 @@ $(document).ready(function() {
while (i<articlesList.length && id != articlesList[i][1]) i++; while (i<articlesList.length && id != articlesList[i][1]) i++;
article_data = articlesList[i]; article_data = articlesList[i];
var amount_euro = - article_data[3] * nb ; var amount_euro = - article_data[3] * nb ;
if (settings['addcost_for'] && settings['addcost_amount'] && account_data['trigramme'] != settings['addcost_for']) if (settings['addcost_for']
amount_euro -= settings['addcost_amount'] * nb; && settings['addcost_amount']
&& account_data['trigramme'] != settings['addcost_for']
&& article_data[5])
amount_euro -= settings['addcost_amount'] * nb;
var reduc_divisor = 1; var reduc_divisor = 1;
if (account_data['is_cof']) if (account_data['is_cof'])
reduc_divisor = 1 + settings['subvention_cof'] / 100; reduc_divisor = 1 + settings['subvention_cof'] / 100;
return amount_euro / reduc_divisor; return (amount_euro / reduc_divisor).toFixed(2);
} }
function addPurchase(id, nb) { function addPurchase(id, nb) {
@ -850,7 +853,7 @@ $(document).ready(function() {
} }
}); });
if (!existing) { if (!existing) {
var amount_euro = amountEuroPurchase(id, nb).toFixed(2); var amount_euro = amountEuroPurchase(id, nb);
var index = addPurchaseToFormset(article_data[1], nb, amount_euro); var index = addPurchaseToFormset(article_data[1], nb, amount_euro);
article_basket_html = $(item_basket_default_html); article_basket_html = $(item_basket_default_html);
article_basket_html article_basket_html

View file

@ -36,6 +36,12 @@
</div> </div>
</div> </div>
<div class="buttons"> <div class="buttons">
{% if account.user == request.user %}
<ul class='nav nav-pills nav-justified'>
<li class="active"><a data-toggle="pill" href="#tab_stats">Statistiques</a></li>
<li><a data-toggle="pill" href="#tab_history">Historique</a></li>
</ul>
{% endif %}
<a class="btn btn-primary btn-lg" href="{% url 'kfet.account.update' account.trigramme %}"> <a class="btn btn-primary btn-lg" href="{% url 'kfet.account.update' account.trigramme %}">
Modifier Modifier
</a> </a>

View file

@ -66,11 +66,13 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% if not perms.kfet.add_order %}
<input type="password" name="KFETPASSWORD">
{% endif %}
{{ formset.management_form }} {{ formset.management_form }}
<input type="submit" class="btn btn-primary btn-lg btn-block" value="Envoyer"> {% if not perms.kfet.add_inventory %}
<div class='auth-form form-horizontal'>
{% include "kfet/form_authentication_snippet.html" %}
</div>
{% endif %}
<input type="submit" value="Enregistrer" class="btn btn-primary btn-lg btn-block">
</form> </form>
</div> </div>
</div> </div>

View file

@ -42,11 +42,13 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% if not perms.kfet.order_to_inventory %}
<input type="password" name="KFETPASSWORD">
{% endif %}
{{ formset.management_form }} {{ formset.management_form }}
<input type="submit" class="btn btn-primary btn-lg btn-block" value="Enregistrer"> {% if not perms.kfet.add_inventory %}
<div class='auth-form form-horizontal'>
{% include "kfet/form_authentication_snippet.html" %}
</div>
{% endif %}
<input type="submit" value="Enregistrer" class="btn btn-primary btn-lg btn-block">
</form> </form>
</div> </div>
</div> </div>

70
kfet/tests.py Normal file
View file

@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
from unittest.mock import patch
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

@ -8,7 +8,7 @@ from kfet.decorators import teamkfet_required
urlpatterns = [ urlpatterns = [
url(r'^$', views.Home.as_view(), url(r'^$', views.Home.as_view(),
name = 'kfet.home'), name='kfet.home'),
url(r'^login/genericteam$', views.login_genericteam, url(r'^login/genericteam$', views.login_genericteam,
name='kfet.login.genericteam'), name='kfet.login.genericteam'),
url(r'^history$', views.history, url(r'^history$', views.history,
@ -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(r'^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(r'^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
@ -134,6 +125,14 @@ urlpatterns = [
# Article urls # Article urls
# ----- # -----
# Category - General
url('^categories/$',
teamkfet_required(views.CategoryList.as_view()),
name='kfet.category'),
# Category - Update
url('^categories/(?P<pk>\d+)/edit$',
teamkfet_required(views.CategoryUpdate.as_view()),
name='kfet.category.update'),
# Article - General # Article - General
url('^articles/$', url('^articles/$',
teamkfet_required(views.ArticleList.as_view()), teamkfet_required(views.ArticleList.as_view()),
@ -149,20 +148,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

File diff suppressed because it is too large Load diff

View file

@ -21,3 +21,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