Merge branch 'Aufinal/bda_revente' of git.eleves.ens.fr:cof-geek/gestioCOF into Aufinal/bda_revente

This commit is contained in:
Martin Pépin 2016-09-26 16:03:41 +02:00
commit 37b4a178a7
38 changed files with 649 additions and 126 deletions

View file

@ -47,28 +47,49 @@ gérer la machine virtuelle :
- `vagrant ssh` vous connecte en SSH à la machine virtuelle, dans le dossier
où est installé GestioCOF. Vous pouvez utiliser les commandes Django
habituelles (`manage.py runserver` etc.); toutefois pour lancer le serveur il faut faire
python manage.py runserver 0.0.0.0:8000
car par défaut Django n'écoute que sur l'adresse locale de la machine
virtuelle - or vous voudrez accéder à GestioCOF depuis votre machine
physique.
habituelles (`manage.py runserver` etc.) pour lancer
[le serveur en dev](#lancer-le-serveur-de-développement-standard) par
exemple
**Le dossier avec le code de GestioCOF est partagé entre la machine virtuelle
et votre machine physique : vous pouvez donc utiliser votre éditeur favori pour
coder depuis l'extérieur de la machine virtuelle, et les changements seront
répercutés dans la machine virtuelle.**
#### Lancer le serveur de développement standard
Pour lancer le serveur de développement, il faut faire
python manage.py runserver 0.0.0.0:8000
car par défaut Django n'écoute que sur l'adresse locale de la machine virtuelle
or vous voudrez accéder à GestioCOF depuis votre machine physique. L'url à
entrer dans le navigateur est `localhost:8000`.
#### Serveur de développement type production
Sur la VM Vagrant, un serveur apache est configuré pour servir GestioCOF de
façon similaire à la version en production : on utilise
[Daphne](https://github.com/django/daphne/) et `python manage.py runworker`
derrière un reverse-proxy apache. Le tout est monitoré par
[supervisor](http://supervisord.org/).
Ce serveur se lance tout seul et est accessible en dehors de la VM à l'url
`localhost:8080`. Toutefois il ne se recharge pas tout seul lorsque le code
change, il faut relancer le worker avec `sudo supervisorctl restart worker` pour
visualiser la dernière version du code.
### Installation manuelle
Si vous optez pour une installation manuelle plutôt que d'utiliser Vagrant, il
est fortement conseillé d'utiliser un environnement virtuel pour Python.
Il vous faudra installer mercurial, pip, les librairies de développement de
python, ainsi qu'un client et un serveur MySQL ; sous Debian et dérivées (Ubuntu, ...) :
python, un client et un serveur MySQL ainsi qu'un serveur redis ; sous Debian et
dérivées (Ubuntu, ...) :
sudo apt-get install mercurial python-pip python-dev libmysqlclient-dev
redis-server
Si vous décidez d'utiliser un environnement virtuel Python (virtualenv;
fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF
@ -87,7 +108,7 @@ Vous pouvez maintenant installer les dépendances Python depuis les fichiers
pip install -r requirements.txt -r requirements-devel.txt
Enfin, copiez le fichier `cof/settings_dev.py` dans `cof/settings.py`.
Copiez le fichier `cof/settings_dev.py` dans `cof/settings.py`.
#### Installation avec MySQL

View file

View file

@ -1,6 +0,0 @@
import os
from channels.asgi import get_channel_layer
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cof.settings")
channel_layer = get_channel_layer()

View file

@ -1,7 +0,0 @@
import os, sys
sys.path.append (os.path.expanduser ('~gestion/www'))
os.environ['DJANGO_SETTINGS_MODULE'] = 'cof.settings'
import django.core.handlers.wsgi
application = django.core.handlers.wsgi.WSGIHandler()

View file

@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
"""
WSGI config for myproject project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
"""
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cof.settings")
application = get_wsgi_application()

View file

@ -14,6 +14,7 @@ class Command(BaseCommand):
def handle(self, *args, **options):
now = timezone.now()
self.stdout.write(now)
reventes = SpectacleRevente.objects.all()
for revente in reventes:
# Check si < 24h
@ -22,11 +23,14 @@ class Command(BaseCommand):
now >= revente.date + timedelta(minutes=15) and \
not revente.notif_sent:
revente.mail_shotgun()
self.stdout.write("Mail de disponibilité immédiate envoyé")
# Check si délai de retrait dépassé
elif (now >= revente.date + timedelta(hours=1) and
not revente.notif_sent):
revente.send_notif()
self.stdout.write("Mail d'inscription à une revente envoyé")
# Check si tirage à faire
elif (now >= revente.expiration_time and
not revente.tirage_done):
revente.tirage()
self.stdout.write("Tirage effectué, mails envoyés")

View file

@ -3,6 +3,7 @@
<!doctype html>
<html>
<head>
<base target="_parent"/>
<style>
@font-face {
font-family: josefinsans;
@ -31,6 +32,10 @@
font-weight: 700;
color: #5a5a5a;
}
img{
max-width: 100%;
}
</style>
<meta charset="utf8" />
</head>
@ -50,11 +55,16 @@
<tr>
<td><p style="text-align: left;">{{ show.date|date:"l j F Y - H\hi" }}</p></td><td class="column-2"><p style="text-align: right;">{{ show.slots }} place{{ show.slots|pluralize}} {% if show.slots_description != "" %}({{ show.slots_description }}){% endif %} - {{ show.price }} euro{{ show.price|pluralize}}</p></td>
</tr>
{% if show.vips %}
<tr>
<td colspan="2"><p style="text-align: justify;">{{ show.vips }}</p></td>
</tr>
{% endif %}
<tr>
<td colspan="2">
<p style="text-align: justify;">{{ show.description }}</p>
{% for quote in show.quote_set.all %}
<p style="text-align:center; font-style: italic;">«{{ quote.text }}»{% if show.quote.author %} - {{ quote.author }}{% endif %}</p>
<p style="text-align:center; font-style: italic;">«{{ quote.text }}»{% if quote.author %} - {{ quote.author }}{% endif %}</p>
{% endfor %}
</td>
</tr>
@ -69,7 +79,7 @@
<script>
// Correction de la taille des images
$(document).ready(function() {
/*$(document).ready(function() {
$(".descTable").each(function() {
$(this).width($("body").width());
});
@ -95,7 +105,7 @@
$(this).height(origHeight);
}
});
});
});*/
</script>

View file

@ -71,7 +71,7 @@ def etat_places(request, tirage_id):
def _hash_queryset(queryset):
data = serializers.serialize("json", queryset).encode()
data = serializers.serialize("json", queryset).encode('utf-8')
hasher = hashlib.sha256()
hasher.update(data)
return hasher.hexdigest()
@ -406,7 +406,7 @@ def list_revente(request, tirage_id):
if qset.exists():
# On l'inscrit à l'un des tirages au sort
for revente in qset.all():
if revente.shotgun:
if revente.shotgun and not revente.soldTo:
deja_revente = True
else:
revente.interested.add(participant)
@ -435,12 +435,16 @@ def buy_revente(request, spectacle_id):
revente.delete()
return HttpResponseRedirect(reverse("bda-liste-revente",
args=[tirage.id]))
reventes_shotgun = []
for revente in reventes.all():
if revente.shotgun:
reventes_shotgun.append(revente)
if not reventes.exists():
if reventes_shotgun.empty():
return render(request, "bda-no-revente.html", {})
if request.POST:
revente = random.choice(reventes.all())
revente = random.choice(reventes_shotgun)
revente.soldTo = participant
revente.save()
mail = """Bonjour !

7
cof/asgi.py Normal file
View file

@ -0,0 +1,7 @@
import os
from channels.asgi import get_channel_layer
if "DJANGO_SETTINGS_MODULE" not in os.environ:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cof.settings")
channel_layer = get_channel_layer()

View file

@ -29,7 +29,7 @@ SECRET_KEY = 'q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
ALLOWED_HOSTS = ['127.0.0.1']
# Application definition
@ -123,6 +123,7 @@ USE_TZ = True
# https://docs.djangoproject.com/en/1.8/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = '/var/www/static/'
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static/'),
@ -154,12 +155,12 @@ RAPPEL_REPLY_TO = RAPPEL_FROM
REVENTE_FROM = 'BDA-Revente <bda-revente@ens.fr>'
REVENTE_REPLY_TO = REVENTE_FROM
LOGIN_URL = "/login"
LOGIN_REDIRECT_URL = "/"
LOGIN_URL = "/gestion/login"
LOGIN_REDIRECT_URL = "/gestion/"
CAS_SERVER_URL = 'https://cas.eleves.ens.fr/'
CAS_IGNORE_REFERER = True
CAS_REDIRECT_URL = '/'
CAS_REDIRECT_URL = '/gestion/'
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
@ -178,7 +179,7 @@ CHANNEL_LAYERS = {
"default": {
"BACKEND": "asgi_redis.RedisChannelLayer",
"CONFIG": {
"hosts": [("redis://:redis_password@127.0.0.1:6379/0")],
"hosts": [("localhost", 6379)],
},
"ROUTING": "cof.routing.channel_routing",
}

View file

@ -24,7 +24,7 @@ from gestioncof.autocomplete import autocomplete
autocomplete_light.autodiscover()
admin.autodiscover()
urlpatterns = [
my_urlpatterns = [
# Page d'accueil
url(r'^$', gestioncof_views.home, name='home'),
# Le BdA
@ -88,3 +88,7 @@ urlpatterns = [
else [])
# Si on est en production, MEDIA_ROOT est servi par Apache.
# Il faut dire à Django de servir MEDIA_ROOT lui-même en développement.
urlpatterns = [
url(r'^gestion/', include(my_urlpatterns))
]

View file

@ -10,8 +10,10 @@ from django.db.models import Q
from django.contrib.auth.models import User
from gestioncof.models import CofProfile, Clipper
from gestioncof.decorators import buro_required
@buro_required
def autocomplete(request):
if "q" not in request.GET:
raise Http404

View file

@ -135,7 +135,7 @@ def _finalize_traitement(request, demande, proposals, proposed_for,
unsatisfied, attribdata, redo=False, errors=None):
proposals = proposals.items()
proposed_for = proposed_for.items()
attribdata = attribdata.items()
attribdata = list(attribdata.items())
proposed_mails = _generate_eleve_email(demande, proposed_for)
mainmail = render_template("petits-cours-mail-demandeur.txt",
{"proposals": proposals,
@ -153,7 +153,8 @@ def _finalize_traitement(request, demande, proposals, proposed_for,
"proposed_mails": proposed_mails,
"mainmail": mainmail,
"attribdata":
base64.b64encode(simplejson.dumps(attribdata)),
base64.b64encode(simplejson.dumps(attribdata)
.encode('utf_8')),
"redo": redo,
"errors": errors,
})

View file

@ -346,7 +346,7 @@ def registration_form2(request, login_clipper=None, username=None):
registration_set_ro_fields(user_form, profile_form)
# events & clubs
event_formset = EventFormset(events=events, prefix='events')
clubs_form = ClubsForm(initial={'clubs': member.clubs.all()})
clubs_form = ClubsForm()
if username:
member = get_object_or_404(User, username=username)
(profile, _) = CofProfile.objects.get_or_create(user=member)

View file

@ -0,0 +1 @@
default_app_config = 'kfet.apps.KFetConfig'

14
kfet/apps.py Normal file
View file

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
from __future__ import (absolute_import, division,
print_function, unicode_literals)
from builtins import *
from django.apps import AppConfig
class KFetConfig(AppConfig):
name = 'kfet'
verbose_name = "Application K-Fêt"
def ready(self):
import kfet.signals

View file

@ -8,8 +8,10 @@ from django.shortcuts import render
from django.http import Http404
from django.db.models import Q
from gestioncof.models import User, Clipper
from kfet.decorators import teamkfet_required
from kfet.models import Account
@teamkfet_required
def account_create(request):
if "q" not in request.GET:
raise Http404

View file

@ -18,7 +18,7 @@ class KFetBackend(object):
return None
try:
password_sha256 = hashlib.sha256(password.encode()).hexdigest()
password_sha256 = hashlib.sha256(password.encode('utf-8')).hexdigest()
account = Account.objects.get(password=password_sha256)
user = account.cofprofile.user
except Account.DoesNotExist:

View file

@ -8,7 +8,7 @@ from channels.routing import route, route_class
from kfet import consumers
channel_routing = [
route_class(consumers.KPsul, path=r"^/ws/k-fet/k-psul/$"),
route_class(consumers.KPsul, path=r"^/gestion/ws/k-fet/k-psul/$"),
#route("websocket.connect", ws_kpsul_history_connect),
#route('websocket.receive', ws_message)
]

16
kfet/signals.py Normal file
View file

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
from __future__ import (absolute_import, division,
print_function, unicode_literals)
from builtins import *
from django.contrib import messages
from django.contrib.auth.signals import user_logged_in
from django.core.urlresolvers import reverse
from django.dispatch import receiver
@receiver(user_logged_in)
def messages_on_login(sender, request, user, **kwargs):
if (not user.username == 'kfet_genericteam'
and user.has_perm('kfet.is_team')):
messages.info(request, '<a href="%s">Connexion en utilisateur partagé ?</a>' % reverse('kfet.login.genericteam'), extra_tags='safe')

View file

@ -263,3 +263,81 @@ textarea {
display:block;
padding:5px 20px;
}
/*
* Messages
*/
.messages .alert {
padding:10px 15px;
margin:0;
border:0;
border-radius:0;
}
.messages .alert-dismissible {
padding-right:35px;
}
.messages .alert .close {
top:0;
right:0;
}
.messages .alert-info {
color:inherit;
background-color:#ccc;
}
.messages .alert-error {
color:inherit;
background-color:rgba(200,16,46,0.2);
}
.messages .alert-success {
color:#333;
}
/*
* Help
*/
.help {
display:none;
position:fixed;
top:50px;
left:0;
right:0;
bottom:0;
overflow:auto;
background:rgba(51,51,51,0.3);
z-index:500;
}
.help-box {
margin-top:30px;
padding-top:1px;
padding-bottom:15px;
background:rgba(51,51,51,0.7);
color:#fff;
}
@media (max-width:768px) {
.help-box {
margin:20px 15px;
}
}
.help h2 {
padding:0 15px 20px;
border-bottom:1px solid #999;
text-align:center;
}
.help .row > div {
padding-right:0;
}
.help h4 {
margin:15px 0;
}

View file

@ -319,6 +319,11 @@ input[type=number]::-webkit-outer-spin-button {
padding-left:20px;
}
#articles_data .article:hover {
background:rgba(200,16,46,0.3);
cursor:pointer;
}
/* Second part - Left - bottom */
.kpsul_middle_left_bottom {

View file

@ -66,8 +66,11 @@ function getErrorsHtml(data) {
content += '</ul>';
}
if ('negative' in data['errors']) {
var url_base = "{% url 'kfet.account.update' LIQ}";
url_base = base_url(0, url_base.length-8);
if (window.location.pathname.startsWith('/gestion/')) {
var url_base = '/gestion/k-fet/accounts/';
} else {
var url_base = '/k-fet/accounts/';
}
for (var i=0; i<data['errors']['negative'].length; i++) {
content += '<a class="btn btn-primary" href="'+url_base+data['errors']['negative'][i]+'/edit" target="_blank" style="width:100%">Autorisation de négatif requise pour '+data['errors']['negative'][i]+'</a>';
}
@ -110,4 +113,3 @@ function requestAuth(data, callback, focus_next = null) {
}
});
}

View file

@ -38,6 +38,7 @@
<li>
<a href="{% url "kfet.account.create.fromclipper" clipper.username %}">
{{ clipper|highlight_clipper:q }}
</a>
</li>
{% endfor %}
{% endif %}

View file

@ -59,7 +59,7 @@
<td>{{ neg.account.name }}</td>
<td class="text-right">{{ neg.account.balance|floatformat:2 }}€</td>
<td class="text-right">
{% if neg.account.balance_offset %}
{% if neg.balance_offset %}
{{ neg.account.real_balance|floatformat:2 }}€
{% endif %}
</td>

View file

@ -40,5 +40,13 @@
{% block content %}{% endblock %}
{% include "kfet/base_footer.html" %}
</div>
<div class="help">
<div class="help-box col-sm-10 col-sm-offset-1 col-md-8 col-md-offset-2">
<div class="help-content">
<h2>Aide</h2>
{% block help %}{% endblock %}
</div>
</div>
</div>
</body>
</html>

View file

@ -1,7 +1,16 @@
{% if messages %}
<ul class="messages">
<div class="row messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
{% endfor %}
</ul>
<div class="col-sm-12 item">
<div class="alert alert-{{ message.level_tag }} alert-dismissible fade in{% if message.tags %} {{ message.tags }}{% endif %}">
<button type="button" class="close" data-dismiss="alert"><span aria-hidden="true">&times;</span></button>
{% if 'safe' in message.tags %}
{{ message|safe }}
{% else %}
{{ message }}
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% endif %}

View file

@ -19,8 +19,59 @@
{% block content-header %}{% endblock %}
{% block help %}
<div class="row">
<div class="col-md-6">
<div class="block">
<h4>Opérations</h4>
<div class="row">
<div class="col-xs-3"><b>F3</b></div>
<div class="col-xs-9">Charge</div>
</div>
<div class="row">
<div class="col-xs-3"><b>Shift + F3</b></div>
<div class="col-xs-9">Retrait</div>
</div>
<div class="row">
<div class="col-xs-3"><b>F8</b></div>
<div class="col-xs-9">Edition</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="block">
<h4>Général</h4>
<div class="row">
<div class="col-xs-3"><b>F1</b></div>
<div class="col-xs-9">Reset</div>
</div>
<div class="row">
<div class="col-xs-3"><b>F2</b></div>
<div class="col-xs-9">Reset compte</div>
</div>
<div class="row">
<div class="col-xs-3"><b>Shift + F2</b></div>
<div class="col-xs-9">Reset panier</div>
</div>
<div class="row">
<div class="col-xs-3"><b>F9</b></div>
<div class="col-xs-9">Majoration</div>
</div>
<div class="row">
<div class="col-xs-3"><b>F10</b></div>
<div class="col-xs-9">Hard reset</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block content %}
{% include 'kfet/base_messages.html' %}
<div class="row kpsul_top">
<div class="col-sm-8">
<div class="row" id="account">
@ -69,7 +120,7 @@
<button role="button" class="btn" id="ask_addcost">Major.</button>
</div>
<div id="article_selection">
<input type="text" id="article_autocomplete">
<input type="text" id="article_autocomplete" autocomplete="off">
<input type="number" id="article_number" step="1" min="1">
<input type="hidden" id="article_id" value="">
</div>
@ -198,8 +249,9 @@ $(document).ready(function() {
}
if (account_data['id'] == 0) {
var trigramme = triInput.val().toUpperCase();
var url_base = '{% url 'kfet.account.create' %}'
if (isValidTrigramme(trigramme)) {
buttons += '<a href="/k-fet/accounts/new?trigramme='+trigramme+'" class="btn btn-primary" target="_blank" title="Créer"><span class="glyphicon glyphicon-plus"></span></a>';
buttons += '<a href="'+url_base+'?trigramme='+trigramme+'" class="btn btn-primary" target="_blank" title="Créer"><span class="glyphicon glyphicon-plus"></span></a>';
}
}
account_container.find('.buttons').html(buttons);
@ -504,17 +556,17 @@ $(document).ready(function() {
function addArticle(article) {
var article_html = $(article_default_html);
article_html.attr('data-article', article['id']);
article_html.attr('data-category', article['category_id']);
article_html.attr('id', 'data-article-'+article['id']);
article_html.addClass('data-category-'+article['category_id']);
for (var elem in article) {
article_html.find('.'+elem).text(article[elem])
}
article_html.find('.price').text(amountToUKF(article['price'], false));
var category_html = articles_container
.find('.category[data-category='+article['category_id']+']');
.find('#data-category-'+article['category_id']);
if (category_html.length == 0) {
category_html = $(article_category_default_html);
category_html.attr('data-category', article['category_id']);
category_html.attr('id', 'data-category-'+article['category_id']);
category_html.find('td').text(article['category__name']);
var added = false;
articles_container.find('.category').each(function() {
@ -526,16 +578,14 @@ $(document).ready(function() {
});
if (!added) articles_container.append(category_html);
}
var added = false;
var $after = articles_container.find('#data-category-'+article['category_id']);
articles_container
.find('.article[data-category='+article['category_id']+']').each(function() {
if (article['name'].toLowerCase < $('.name', this).text().toLowerCase()) {
$(this).before(article_html);
added = true;
.find('.article.data-category-'+article['category_id']).each(function() {
if (article['name'].toLowerCase < $('.name', this).text().toLowerCase())
return false;
}
$after = $(this);
});
if (!added) articles_container.find('.category[data-category='+article['category_id']+']').after(article_html);
$after.after(article_html);
// Pour l'autocomplétion
articlesList.push([article['name'],article['id'],article['category_id'],article['price']]);
}
@ -597,17 +647,17 @@ $(document).ready(function() {
var categories_to_display = [];
for (var i=0; i<articlesList.length; i++) {
if (array.indexOf(articlesList[i]) > -1) {
articles_container.find('[data-article='+articlesList[i][1]+']').show();
articles_container.find('#data-article-'+articlesList[i][1]).show();
if (categories_to_display.indexOf(articlesList[i][2]) == -1)
categories_to_display.push(articlesList[i][2]);
} else {
articles_container.find('[data-article='+articlesList[i][1]+']').hide();
articles_container.find('#data-article-'+articlesList[i][1]).hide();
}
}
articles_container.find('.category').hide();
for (var i=0; i<categories_to_display.length; i++) {
articles_container
.find('.category[data-category='+categories_to_display[i]+']')
.find('#data-category-'+categories_to_display[i])
.show();
}
}
@ -635,7 +685,13 @@ $(document).ready(function() {
return false;
}
articleSelect.on('keypress', function(e) {
// A utiliser après la sélection d'un article
function goToArticleNb() {
articleNb.val('1');
articleNb.focus().select();
}
articleSelect.on('keydown', function(e) {
var text = articleSelect.val();
// Comportement normal pour ces touches
if (normalKeys.test(e.keyCode) || e.ctrlKey) {
@ -648,14 +704,26 @@ $(document).ready(function() {
articleSelect.val('');
}
return true;
} else if (e.charCode !== 0) {
if (updateMatchedArticles(text+e.key)) {
articleNb.val('1');
articleNb.focus().select();
}
if (updateMatchedArticles(text+e.key))
goToArticleNb();
return false;
});
function getArticleId($article) {
return $article.attr('id').split('-')[2];
}
return false;
function getArticleName($article) {
return $article.find('.name').text();
}
// Sélection des articles à la souris/tactile
articles_container.on('click', '.article', function() {
articleId.val(getArticleId($(this)));
articleSelect.val(getArticleName($(this)));
displayMatchedArticles(articlesList);
goToArticleNb();
});
function is_nb_ok(nb) {
@ -1088,7 +1156,9 @@ $(document).ready(function() {
websocket_msg_default = {'opegroups':[],'opes':[],'checkouts':[],'articles':[]}
var websocket_protocol = window.location.protocol == 'https:' ? 'wss' : 'ws';
socket = new ReconnectingWebSocket(websocket_protocol+"://" + window.location.host + "/ws/k-fet/k-psul/");
var location_host = window.location.host;
var location_url = window.location.pathname.startsWith('/gestion/') ? location_host + '/gestion' : location_host;
socket = new ReconnectingWebSocket(websocket_protocol+"://" + location_url + "/ws/k-fet/k-psul/");
socket.onmessage = function(e) {
data = $.extend({}, websocket_msg_default, JSON.parse(e.data));
@ -1139,7 +1209,7 @@ $(document).ready(function() {
function hardReset(give_tri_focus=true) {
coolReset(give_tri_focus);
resetCheckout();
checkoutInput.trigger('change');
resetArticles();
khistory.reset();
resetSettings();
@ -1163,6 +1233,17 @@ $(document).ready(function() {
$(document).on('keydown', function(e) {
switch (e.keyCode) {
case 27:
// Escape - Hide help
$('.help').hide('fast');
return false;
case 72:
if (e.ctrlKey) {
// Ctrl+H - Display help
e.preventDefault();
$('.help').show('fast');
}
return true;
case 112:
// F1 - Cool reset
coolReset();

View file

@ -1,4 +1,13 @@
{% extends 'kfet/base.html' %}
{% load staticfiles %}
{% block extra_head %}
<link rel="stylesheet" style="text/css" href="{% static 'kfet/css/jquery-ui.min.css' %}">
<script type="text/javascript" src="{% static 'kfet/js/js.cookie.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/jquery-confirm.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/kfet.js' %}"></script>
{% endblock %}
{% block title %}Transferts{% endblock %}
{% block content-header-title %}Transferts{% endblock %}
@ -24,13 +33,13 @@
<h2>Liste des transferts</h2>
<div id="history">
{% for transfergroup in transfergroups %}
<div class="opegroup">
<div class="opegroup transfergroup" data-transfergroup="{{ transfergroup.pk }}">
<span>{{ transfergroup.at }}</span>
<span>{{ transfergroup.valid_by.trigramme }}</span>
<span>{{ transfergroup.comment }}</span>
</div>
{% for transfer in transfergroup.transfers.all %}
<div class="ope transfer">
<div class="ope transfer{% if transfer.canceled_at %} canceled{% endif %}" data-transfer="{{ transfer.pk }}" data-transfergroup="{{ transfergroup.pk }}">
<span class="amount">{{ transfer.amount }} €</span>
<span class="from_acc">{{ transfer.from_acc.trigramme }}</span>
<span class="glyphicon glyphicon-arrow-right"></span>
@ -44,4 +53,93 @@
</div>
</div>
<script type="text/javascript">
$(document).ready(function() {
lock = 0;
function displayErrors(html) {
$.alert({
title: 'Erreurs',
content: html,
backgroundDismiss: true,
animation: 'top',
closeAnimation: 'bottom',
keyboardEnabled: true,
});
}
function cancelTransfers(transfers_array, password = '') {
if (lock == 1)
return false
lock = 1;
var data = { 'transfers' : transfers_array }
$.ajax({
dataType: "json",
url : "{% url 'kfet.transfers.cancel' %}",
method : "POST",
data : data,
beforeSend: function ($xhr) {
$xhr.setRequestHeader("X-CSRFToken", csrftoken);
if (password != '')
$xhr.setRequestHeader("KFetPassword", password);
},
})
.done(function(data) {
for (var i=0; i<data['canceled'].length; i++) {
$('#history').find('.transfer[data-transfer='+data['canceled'][i]+']')
.addClass('canceled');
}
$('#history').find('.ui-selected').removeClass('ui-selected');
lock = 0;
})
.fail(function($xhr) {
var data = $xhr.responseJSON;
switch ($xhr.status) {
case 403:
requestAuth(data, function(password) {
cancelTransfers(transfers_array, password);
});
break;
case 400:
displayErrors(getErrorsHtml(data));
break;
}
lock = 0;
});
}
$('#history').selectable({
filter: 'div.transfergroup, div.transfer',
selected: function(e, ui) {
$(ui.selected).each(function() {
if ($(this).hasClass('transfergroup')) {
var transfergroup = $(this).attr('data-transfergroup');
$(this).siblings('.ope').filter(function() {
return $(this).attr('data-transfergroup') == transfergroup
}).addClass('ui-selected');
}
});
},
});
$(document).on('keydown', function (e) {
if (e.keyCode == 46) {
// DEL (Suppr)
var transfers_to_cancel = [];
$('#history').find('.transfer.ui-selected').each(function () {
transfers_to_cancel.push($(this).attr('data-transfer'));
});
if (transfers_to_cancel.length > 0)
cancelTransfers(transfers_to_cancel);
}
});
});
</script>
{% endblock %}

View file

@ -170,6 +170,8 @@ urlpatterns = [
name = 'kfet.transfers.create'),
url(r'^transfers/perform$', views.perform_transfers,
name = 'kfet.transfers.perform'),
url(r'^transfers/cancel$', views.cancel_transfers,
name = 'kfet.transfers.cancel'),
# -----
# Inventories urls

View file

@ -219,7 +219,7 @@ def account_form_set_readonly_fields(user_form, cof_form):
def get_account_create_forms(request=None, username=None, login_clipper=None):
user = None
clipper = None
if login_clipper and not username:
if login_clipper and (login_clipper == username or not username):
# à partir d'un clipper
# le user associé à ce clipper ne devrait pas encore exister
clipper = get_object_or_404(Clipper, username = login_clipper)
@ -399,7 +399,7 @@ def account_update(request, trigramme):
if (request.user.has_perm('kfet.change_account_password')
and pwd_form.is_valid()):
pwd = pwd_form.cleaned_data['pwd1']
pwd_sha256 = hashlib.sha256(pwd.encode()).hexdigest()
pwd_sha256 = hashlib.sha256(pwd.encode('utf-8')).hexdigest()
Account.objects.filter(pk=account.pk).update(
password = pwd_sha256)
messages.success(request, 'Mot de passe mis à jour')
@ -796,7 +796,15 @@ def kpsul(request):
data = {}
data['operationgroup_form'] = KPsulOperationGroupForm()
data['trigramme_form'] = KPsulAccountForm()
data['checkout_form'] = KPsulCheckoutForm()
initial = {}
try:
checkout = Checkout.objects.filter(
is_protected=False, valid_from__lte=timezone.now(),
valid_to__gte=timezone.now()).get()
initial['checkout'] = checkout
except (Checkout.DoesNotExist, Checkout.MultipleObjectsReturned):
pass
data['checkout_form'] = KPsulCheckoutForm(initial=initial)
operation_formset = KPsulOperationFormSet(queryset=Operation.objects.none())
data['operation_formset'] = operation_formset
return render(request, 'kfet/kpsul.html', data)
@ -827,7 +835,6 @@ def kpsul_checkout_data(request):
pk = request.POST.get('pk', 0)
if not pk:
pk = 0
try:
data = (Checkout.objects
.annotate(
last_statement_by_first_name=F('statements__by__cofprofile__user__first_name'),
@ -847,8 +854,8 @@ def kpsul_checkout_data(request):
.filter(pk=pk)
.order_by('statements__at')
.last())
except Checkout.DoesNotExist:
raise http404
if data is None:
raise Http404
return JsonResponse(data)
@teamkfet_required
@ -1402,7 +1409,7 @@ def perform_transfers(request):
transfers = transfer_formset.save(commit = False)
# Initializing vars
required_perms = set('kfet.add_transfer') # Required perms to perform all transfers
required_perms = set(['kfet.add_transfer']) # Required perms to perform all transfers
to_accounts_balances = defaultdict(lambda:0) # For balances of accounts
for transfer in transfers:
@ -1468,6 +1475,105 @@ def perform_transfers(request):
return JsonResponse(data)
@teamkfet_required
def cancel_transfers(request):
# Pour la réponse
data = { 'canceled': [], 'warnings': {}, 'errors': {}}
# Checking if BAD REQUEST (transfers_pk not int or not existing)
try:
# Set pour virer les doublons
transfers_post = set(map(int, filter(None, request.POST.getlist('transfers[]', []))))
except ValueError:
return JsonResponse(data, status=400)
transfers_all = (
Transfer.objects
.select_related('group', 'from_acc', 'from_acc__negative',
'to_acc', 'to_acc__negative')
.filter(pk__in=transfers_post))
transfers_pk = [ transfer.pk for transfer in transfers_all ]
transfers_notexisting = [ transfer for transfer in transfers_post
if transfer not in transfers_pk ]
if transfers_notexisting:
data['errors']['transfers_notexisting'] = transfers_notexisting
return JsonResponse(data, status=400)
transfers_already_canceled = [] # Déjà annulée
transfers = [] # Pas déjà annulée
required_perms = set()
stop_all = False
cancel_duration = Settings.CANCEL_DURATION()
to_accounts_balances = defaultdict(lambda:0) # Modifs à faire sur les balances des comptes
for transfer in transfers_all:
if transfer.canceled_at:
# Transfert déjà annulé, va pour un warning en Response
transfers_already_canceled.append(transfer.pk)
else:
transfers.append(transfer.pk)
# Si transfer il y a plus de CANCEL_DURATION, permission requise
if transfer.group.at + cancel_duration < timezone.now():
required_perms.add('kfet.cancel_old_operations')
# Calcul de toutes modifs à faire en cas de validation
# Pour les balances de comptes
to_accounts_balances[transfer.from_acc] += transfer.amount
to_accounts_balances[transfer.to_acc] += -transfer.amount
if not transfers:
data['warnings']['already_canceled'] = transfers_already_canceled
return JsonResponse(data)
negative_accounts = []
# Checking permissions or stop
for account in to_accounts_balances:
(perms, stop) = account.perms_to_perform_operation(
amount = to_accounts_balances[account])
required_perms |= perms
stop_all = stop_all or stop
if stop:
negative_accounts.append(account.trigramme)
print(required_perms)
print(request.user.get_all_permissions())
if stop_all or not request.user.has_perms(required_perms):
missing_perms = get_missing_perms(required_perms, request.user)
if missing_perms:
data['errors']['missing_perms'] = missing_perms
if stop_all:
data['errors']['negative'] = negative_accounts
return JsonResponse(data, status=403)
canceled_by = required_perms and request.user.profile.account_kfet or None
canceled_at = timezone.now()
with transaction.atomic():
(Transfer.objects.filter(pk__in=transfers)
.update(canceled_by=canceled_by, canceled_at=canceled_at))
for account in to_accounts_balances:
Account.objects.filter(pk=account.pk).update(
balance = F('balance') + to_accounts_balances[account])
account.refresh_from_db()
if account.balance < 0:
if hasattr(account, 'negative'):
if not account.negative.start:
account.negative.start = timezone.now()
account.negative.save()
else:
negative = AccountNegative(
account = account, start = timezone.now())
negative.save()
elif (hasattr(account, 'negative')
and not account.negative.balance_offset):
account.negative.delete()
data['canceled'] = transfers
if transfers_already_canceled:
data['warnings']['already_canceled'] = transfers_already_canceled
return JsonResponse(data)
class InventoryList(ListView):
queryset = (Inventory.objects
.select_related('by', 'order')

29
provisioning/apache.conf Normal file
View file

@ -0,0 +1,29 @@
<VirtualHost *:80>
ServerName default
DocumentRoot /var/www/html
ProxyPreserveHost On
ProxyRequests Off
ProxyPass /static/ !
ProxyPass /media/ !
ProxyPass /ws/ ws://127.0.0.1:8001/ws/
ProxyPass / http://127.0.0.1:8001/
ProxyPassReverse / http://127.0.0.1:8001/
Alias /media /vagrant/media
Alias /static /var/www/static
<Directory /vagrant/media>
Order deny,allow
Allow from all
</Directory>
<Directory /var/www/static>
Order deny,allow
Allow from all
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

View file

@ -9,7 +9,7 @@ DBPASSWD="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
# Installation de paquets utiles
apt-get update && apt-get install -y mercurial python-pip python-dev \
libmysqlclient-dev libjpeg-dev git
libmysqlclient-dev libjpeg-dev git redis-server
# Configuration et installation de mysql. Le mot de passe root est le même que
# le mot de passe pour l'utilisateur local - pour rappel, ceci est une instance
@ -21,9 +21,15 @@ apt-get install -y mysql-server
mysql -uroot -p$DBPASSWD -e "CREATE DATABASE $DBNAME; GRANT ALL PRIVILEGES ON $DBNAME.* TO '$DBUSER'@'localhost' IDENTIFIED BY '$DBPASSWD'"
# Installation de redis-server. Todo: lui mettre un mot de passe
apt-get install -y redis-server
redis-cli config set requirepass redis_password
# Installation et configuration d'Apache
apt-get install -y apache2
a2enmod proxy proxy_http
cp /vagrant/provisioning/apache.conf /etc/apache2/sites-available/gestiocof.conf
a2ensite gestiocof
a2dissite 000-default
service apache2 restart
mkdir /var/www/static
chown -R vagrant:www-data /var/www/static
# Mise en place du .bash_profile pour tout configurer lors du `vagrant ssh`
cat > ~vagrant/.bash_profile <<EOF
@ -54,3 +60,12 @@ sudo -H -u vagrant DJANGO_SETTINGS_MODULE='cof.settings_dev' DBUSER=$DBUSER DBNA
# Installation du cron pour les mails de rappels
sudo -H -u vagrant crontab provisioning/cron.dev
# On installe Daphne et on demande à supervisor de le lancer
pip install daphne
apt-get install -y supervisor
cp /vagrant/provisioning/supervisor.conf /etc/supervisor/conf.d/gestiocof.conf
sed "s/{DBUSER}/$DBUSER/" -i /etc/supervisor/conf.d/gestiocof.conf
sed "s/{DBNAME}/$DBNAME/" -i /etc/supervisor/conf.d/gestiocof.conf
sed "s/{DBPASSWD}/$DBPASSWD/" -i /etc/supervisor/conf.d/gestiocof.conf
service supervisor restart

View file

@ -7,3 +7,4 @@ DBNAME="cof_gestion"
DBPASSWD="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
19 */12 * * * date >> /vagrant/rappels.log ; python /vagrant/manage.py sendrappels >> /vagrant/rappels.log 2>&1
*/5 * * * * python /vagrant/manage.py manage_revente >> /vagrant/reventes.log 2>&1

View file

@ -14,3 +14,14 @@ envoyés).
- Garde les logs peut être une bonne idée.
Exemple : voir le fichier `provisioning/cron.dev`.
## Gestion des mails de revente
Il faut effectuer très régulièrement la commande `manage_reventes` de GestioCOF,
qui gère toutes les actions associées à BdA-Revente : envoi des mails de notification,
tirages.
- Pour l'instant un délai de 5 min est hardcodé
- Garde des logs ; ils vont finir par être assez lourds si on a beaucoup de reventes.
Exemple : provisioning/cron.dev

View file

@ -2,3 +2,4 @@
python manage.py migrate
python manage.py loaddata users root bda gestion sites
python manage.py collectstatic --noinput

View file

@ -0,0 +1,20 @@
[program:worker]
command=/usr/bin/python /vagrant/manage.py runworker
directory=/vagrant/
user=vagrant
environment=DBUSER={DBUSER},DBNAME={DBNAME},DBPASSWD={DBPASSWD},DJANGO_SETTINGS_MODULE="cof.settings_dev"
autostart=true
autorestart=true
redirect_stderr=true
stopasgroup=true
redirect_stderr=true
[program:interface]
command=/usr/local/bin/daphne -b 127.0.0.1 -p 8001 cof.asgi:channel_layer
environment=DBUSER={DBUSER},DBNAME={DBNAME},DBPASSWD={DBPASSWD},DJANGO_SETTINGS_MODULE="cof.settings_dev"
directory=/vagrant/
redirect_stderr=true
autostart=true
autorestart=true
stopasgroup=true
user=vagrant

View file

@ -2,7 +2,7 @@ configparser==3.5.0
Django==1.8
django-autocomplete-light==2.3.3
django-autoslug==1.9.3
django-cas-ng==3.5.4
git+https://github.com/xapantu/django-cas-ng.git#egg=django-cas-ng
django-grappelli==2.8.1
django-recaptcha==1.0.5
mysqlclient==1.3.7