diff --git a/README.md b/README.md index 15f0ec0d..db2cc93b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/apache/__init__.py b/apache/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/apache/asgi.py b/apache/asgi.py deleted file mode 100644 index eb18c7b2..00000000 --- a/apache/asgi.py +++ /dev/null @@ -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() diff --git a/apache/django.wsgi b/apache/django.wsgi deleted file mode 100644 index 7b9c271d..00000000 --- a/apache/django.wsgi +++ /dev/null @@ -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() diff --git a/apache/wsgi.py b/apache/wsgi.py deleted file mode 100644 index 177542a8..00000000 --- a/apache/wsgi.py +++ /dev/null @@ -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() diff --git a/bda/management/commands/manage_reventes.py b/bda/management/commands/manage_reventes.py index 9fea3f6a..f45357b1 100644 --- a/bda/management/commands/manage_reventes.py +++ b/bda/management/commands/manage_reventes.py @@ -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") diff --git a/bda/templates/descriptions.html b/bda/templates/descriptions.html index dd186dac..26da76b6 100644 --- a/bda/templates/descriptions.html +++ b/bda/templates/descriptions.html @@ -3,6 +3,7 @@ + @@ -50,11 +55,16 @@

{{ show.date|date:"l j F Y - H\hi" }}

{{ show.slots }} place{{ show.slots|pluralize}} {% if show.slots_description != "" %}({{ show.slots_description }}){% endif %} - {{ show.price }} euro{{ show.price|pluralize}}

- + {% if show.vips %} + +

{{ show.vips }}

+ + {% endif %} +

{{ show.description }}

{% for quote in show.quote_set.all %} -

«{{ quote.text }}»{% if show.quote.author %} - {{ quote.author }}{% endif %}

+

«{{ quote.text }}»{% if quote.author %} - {{ quote.author }}{% endif %}

{% endfor %} @@ -69,7 +79,7 @@ diff --git a/bda/views.py b/bda/views.py index bac7415d..79212710 100644 --- a/bda/views.py +++ b/bda/views.py @@ -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 ! diff --git a/cof/asgi.py b/cof/asgi.py new file mode 100644 index 00000000..a34621c7 --- /dev/null +++ b/cof/asgi.py @@ -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() diff --git a/cof/settings_dev.py b/cof/settings_dev.py index 45ea010b..7d270648 100644 --- a/cof/settings_dev.py +++ b/cof/settings_dev.py @@ -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 ' 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", } diff --git a/cof/urls.py b/cof/urls.py index ca7ea247..263fc3a0 100644 --- a/cof/urls.py +++ b/cof/urls.py @@ -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)) +] diff --git a/gestioncof/autocomplete.py b/gestioncof/autocomplete.py index 5b4616be..ed0a1e5a 100644 --- a/gestioncof/autocomplete.py +++ b/gestioncof/autocomplete.py @@ -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 diff --git a/gestioncof/petits_cours_views.py b/gestioncof/petits_cours_views.py index 7d54766e..1a31115d 100644 --- a/gestioncof/petits_cours_views.py +++ b/gestioncof/petits_cours_views.py @@ -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, }) diff --git a/gestioncof/views.py b/gestioncof/views.py index 3148308a..80f528f7 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -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) diff --git a/kfet/__init__.py b/kfet/__init__.py index e69de29b..5d6c8f97 100644 --- a/kfet/__init__.py +++ b/kfet/__init__.py @@ -0,0 +1 @@ +default_app_config = 'kfet.apps.KFetConfig' diff --git a/kfet/apps.py b/kfet/apps.py new file mode 100644 index 00000000..29f9f98e --- /dev/null +++ b/kfet/apps.py @@ -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 diff --git a/kfet/autocomplete.py b/kfet/autocomplete.py index e4203bd1..2a24a51e 100644 --- a/kfet/autocomplete.py +++ b/kfet/autocomplete.py @@ -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 diff --git a/kfet/backends.py b/kfet/backends.py index 62b2d820..3729f1bd 100644 --- a/kfet/backends.py +++ b/kfet/backends.py @@ -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: diff --git a/kfet/routing.py b/kfet/routing.py index e7bcca55..9c816c92 100644 --- a/kfet/routing.py +++ b/kfet/routing.py @@ -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) ] diff --git a/kfet/signals.py b/kfet/signals.py new file mode 100644 index 00000000..3dd4d677 --- /dev/null +++ b/kfet/signals.py @@ -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, 'Connexion en utilisateur partagé ?' % reverse('kfet.login.genericteam'), extra_tags='safe') diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css index c6ee9ff6..f3e9bf8c 100644 --- a/kfet/static/kfet/css/index.css +++ b/kfet/static/kfet/css/index.css @@ -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; +} diff --git a/kfet/static/kfet/css/kpsul.css b/kfet/static/kfet/css/kpsul.css index b15f6de8..9fd53604 100644 --- a/kfet/static/kfet/css/kpsul.css +++ b/kfet/static/kfet/css/kpsul.css @@ -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 { diff --git a/kfet/static/kfet/js/kfet.js b/kfet/static/kfet/js/kfet.js index 7aa1d963..dbfba0b2 100644 --- a/kfet/static/kfet/js/kfet.js +++ b/kfet/static/kfet/js/kfet.js @@ -66,8 +66,11 @@ function getErrorsHtml(data) { content += ''; } 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; iAutorisation de négatif requise pour '+data['errors']['negative'][i]+''; } @@ -110,4 +113,3 @@ function requestAuth(data, callback, focus_next = null) { } }); } - diff --git a/kfet/templates/kfet/account_create_autocomplete.html b/kfet/templates/kfet/account_create_autocomplete.html index b99abecf..1185c3a8 100644 --- a/kfet/templates/kfet/account_create_autocomplete.html +++ b/kfet/templates/kfet/account_create_autocomplete.html @@ -38,6 +38,7 @@
  • {{ clipper|highlight_clipper:q }} +
  • {% endfor %} {% endif %} diff --git a/kfet/templates/kfet/account_negative.html b/kfet/templates/kfet/account_negative.html index 77fdf118..5f77b8f0 100644 --- a/kfet/templates/kfet/account_negative.html +++ b/kfet/templates/kfet/account_negative.html @@ -59,7 +59,7 @@ {{ neg.account.name }} {{ neg.account.balance|floatformat:2 }}€ - {% if neg.account.balance_offset %} + {% if neg.balance_offset %} {{ neg.account.real_balance|floatformat:2 }}€ {% endif %} diff --git a/kfet/templates/kfet/base.html b/kfet/templates/kfet/base.html index c1db0c26..173a5fb7 100644 --- a/kfet/templates/kfet/base.html +++ b/kfet/templates/kfet/base.html @@ -40,5 +40,13 @@ {% block content %}{% endblock %} {% include "kfet/base_footer.html" %} +
    +
    +
    +

    Aide

    + {% block help %}{% endblock %} +
    +
    +
    diff --git a/kfet/templates/kfet/base_messages.html b/kfet/templates/kfet/base_messages.html index d2bb5061..440b8c10 100644 --- a/kfet/templates/kfet/base_messages.html +++ b/kfet/templates/kfet/base_messages.html @@ -1,7 +1,16 @@ {% if messages %} - +
    + {% for message in messages %} +
    +
    + + {% if 'safe' in message.tags %} + {{ message|safe }} + {% else %} + {{ message }} + {% endif %} +
    +
    + {% endfor %} +
    {% endif %} diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index 1694d57f..0eceebd2 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -19,8 +19,59 @@ {% block content-header %}{% endblock %} +{% block help %} + +
    +
    +
    +

    Opérations

    +
    +
    F3
    +
    Charge
    +
    +
    +
    Shift + F3
    +
    Retrait
    +
    +
    +
    F8
    +
    Edition
    +
    +
    +
    +
    +
    +

    Général

    +
    +
    F1
    +
    Reset
    +
    +
    +
    F2
    +
    Reset compte
    +
    +
    +
    Shift + F2
    +
    Reset panier
    +
    +
    +
    F9
    +
    Majoration
    +
    +
    +
    F10
    +
    Hard reset
    +
    +
    +
    +
    + +{% endblock %} + {% block content %} +{% include 'kfet/base_messages.html' %} +
    @@ -69,7 +120,7 @@
    - +
    @@ -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 += ''; + buttons += ''; } } 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 -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 0 && nb <= 24; } @@ -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(); diff --git a/kfet/templates/kfet/transfers.html b/kfet/templates/kfet/transfers.html index 1875a21e..cbdf0fe3 100644 --- a/kfet/templates/kfet/transfers.html +++ b/kfet/templates/kfet/transfers.html @@ -1,4 +1,13 @@ {% extends 'kfet/base.html' %} +{% load staticfiles %} + +{% block extra_head %} + + + + + +{% endblock %} {% block title %}Transferts{% endblock %} {% block content-header-title %}Transferts{% endblock %} @@ -24,13 +33,13 @@

    Liste des transferts

    {% for transfergroup in transfergroups %} -
    +
    {{ transfergroup.at }} {{ transfergroup.valid_by.trigramme }} {{ transfergroup.comment }}
    {% for transfer in transfergroup.transfers.all %} -
    +
    {{ transfer.amount }} € {{ transfer.from_acc.trigramme }} @@ -44,4 +53,93 @@
    + + {% endblock %} diff --git a/kfet/urls.py b/kfet/urls.py index e0bacf9a..9b9ebf21 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -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 diff --git a/kfet/views.py b/kfet/views.py index 736dbe8a..3f1e1f4d 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -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,28 +835,27 @@ 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'), - last_statement_by_last_name=F('statements__by__cofprofile__user__last_name'), - last_statement_by_trigramme=F('statements__by__trigramme'), - last_statement_balance=F('statements__balance_new'), - last_statement_at=F('statements__at')) - .values( - 'id', 'name', 'balance', 'valid_from', 'valid_to', - 'last_statement_balance', 'last_statement_at', - 'last_statement_by_trigramme', 'last_statement_by_last_name', - 'last_statement_by_first_name') - .select_related( - 'statements' - 'statements__by', - 'statements__by__cofprofile__user') - .filter(pk=pk) - .order_by('statements__at') - .last()) - except Checkout.DoesNotExist: - raise http404 + data = (Checkout.objects + .annotate( + last_statement_by_first_name=F('statements__by__cofprofile__user__first_name'), + last_statement_by_last_name=F('statements__by__cofprofile__user__last_name'), + last_statement_by_trigramme=F('statements__by__trigramme'), + last_statement_balance=F('statements__balance_new'), + last_statement_at=F('statements__at')) + .values( + 'id', 'name', 'balance', 'valid_from', 'valid_to', + 'last_statement_balance', 'last_statement_at', + 'last_statement_by_trigramme', 'last_statement_by_last_name', + 'last_statement_by_first_name') + .select_related( + 'statements' + 'statements__by', + 'statements__by__cofprofile__user') + .filter(pk=pk) + .order_by('statements__at') + .last()) + 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') diff --git a/provisioning/apache.conf b/provisioning/apache.conf new file mode 100644 index 00000000..001c6ec9 --- /dev/null +++ b/provisioning/apache.conf @@ -0,0 +1,29 @@ + + 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 + + Order deny,allow + Allow from all + + + Order deny,allow + Allow from all + + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/provisioning/bootstrap.sh b/provisioning/bootstrap.sh index b6df6caa..f072e6fc 100644 --- a/provisioning/bootstrap.sh +++ b/provisioning/bootstrap.sh @@ -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 <> /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 diff --git a/provisioning/cron.md b/provisioning/cron.md index 8b3f608e..840a8716 100644 --- a/provisioning/cron.md +++ b/provisioning/cron.md @@ -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 diff --git a/provisioning/prepare_django.sh b/provisioning/prepare_django.sh index ef26235e..8988e834 100644 --- a/provisioning/prepare_django.sh +++ b/provisioning/prepare_django.sh @@ -2,3 +2,4 @@ python manage.py migrate python manage.py loaddata users root bda gestion sites +python manage.py collectstatic --noinput diff --git a/provisioning/supervisor.conf b/provisioning/supervisor.conf new file mode 100644 index 00000000..4c46b952 --- /dev/null +++ b/provisioning/supervisor.conf @@ -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 diff --git a/requirements.txt b/requirements.txt index d7cada42..2d1109eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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