Compare commits

...

67 commits

Author SHA1 Message Date
thubrecht 2126224e15 Merge pull request 'Thubrecht/daphne' (#815) from thubrecht/daphne into master
Reviewed-on: DGNum/gestioCOF#815
2024-07-30 18:40:56 +02:00
Tom Hubrecht d6109a9312 dev: update requirements and directly read the files 2024-07-12 14:54:14 +02:00
Tom Hubrecht a69bd0426f kfet/open: Add comment to test 2024-07-12 14:54:14 +02:00
Tom Hubrecht 81a4dbef7c dev: set explicitely DJANGO_SETTINGS_MODULE 2024-07-12 14:54:14 +02:00
Tom Hubrecht 4fedf3453d kfet: remove redundant static tag 2024-07-12 14:54:14 +02:00
Tom Hubrecht 8607d77c84 dev: update requirements 2024-07-12 14:54:14 +02:00
Tom Hubrecht bc55a3067e Remove useless migrations 2024-07-12 14:54:14 +02:00
Tom Hubrecht dd68ad91cd Update django-hCaptcha 2024-07-12 14:54:14 +02:00
Tom Hubrecht 2f71246509 assertDictContainsSubset is deprecated 2024-07-12 14:54:14 +02:00
Tom Hubrecht a20a1c11d6 Explicitely set transform=repr in assertQuerysetEquals 2024-07-12 14:54:14 +02:00
Tom Hubrecht 0e1ff1765a assertEquals is deprecated 2024-07-12 14:54:14 +02:00
Tom Hubrecht 177d413f4c Use AutoField instead of BigAutoField 2024-07-12 14:54:14 +02:00
Tom Hubrecht 01dd16c795 url -> re_path 2024-07-12 14:54:14 +02:00
Tom Hubrecht 84c87c1b4b Replace unique_together by UniqueConstraint 2024-07-12 14:54:14 +02:00
Tom Hubrecht 1b143b322f Update changelog 2024-07-12 14:54:14 +02:00
Tom Hubrecht f9456e3c29 cof has been renamed to gestioasso 2024-07-12 14:54:14 +02:00
Tom Hubrecht dd1350f1c2 Fix typo 2024-07-12 14:54:14 +02:00
Tom Hubrecht 8f4cb68d31 Only run kf tests in sequential mode (to fix issues with channels) 2024-07-12 14:54:14 +02:00
Tom Hubrecht 3a3f96a8df Try not running tests in parallel 2024-07-12 14:54:14 +02:00
Tom Hubrecht 7d8926e459 Try to flush old messages in tests 2024-07-12 14:54:14 +02:00
Tom Hubrecht 3fee014384 Update kfet.open tests 2024-07-12 14:54:14 +02:00
Tom Hubrecht cd351786bb Remove default_app_config 2024-07-12 14:54:14 +02:00
Tom Hubrecht 7362c2fa2a Update django-djconfig 2024-07-12 14:54:14 +02:00
Tom Hubrecht efbc947145 Change backend name as the old one is deprecated 2024-07-12 14:54:14 +02:00
Tom Hubrecht 0c45262fbc ugettext -> gettext 2024-07-12 14:54:14 +02:00
Tom Hubrecht cabd277b4a Update redis, and implement a custom channel layer to send datetime/decimal objects 2024-07-12 14:54:14 +02:00
Tom Hubrecht 4108efe8c9 Fix kfet.ope tests 2024-07-12 14:54:14 +02:00
Tom Hubrecht 693e4252d5 Fix kpsul tests 2024-07-12 14:54:14 +02:00
Tom Hubrecht 3aa928e8f0 Simplify group_send for kpsul 2024-07-12 14:54:14 +02:00
Tom Hubrecht 5e2e68960b Channels 3 2024-07-12 14:54:14 +02:00
Tom Hubrecht 91e9beee11 Add default django asgi handler 2024-07-12 14:54:14 +02:00
Tom Hubrecht ac286209ed Fix app import 2024-07-12 14:54:14 +02:00
Tom Hubrecht b03cda5962 keyOrder is deprecated, using a list in Meta gives the correct order 2024-07-12 14:54:14 +02:00
Tom Hubrecht 4feb567af7 Update django-redis-cache and fix vagrant setup 2024-07-12 14:54:14 +02:00
Tom Hubrecht 1ac47885d0 Update django version 2024-07-12 14:54:14 +02:00
Tom Hubrecht 071c810605 Update base consumers 2024-07-12 14:54:14 +02:00
Tom Hubrecht 7f00ce0ff1 Add default http router 2024-07-12 14:54:14 +02:00
Tom Hubrecht 95136cb4eb Update daphne version 2024-07-12 14:54:14 +02:00
Tom Hubrecht e299997aa8 Update to django channels 2 2024-07-12 14:54:14 +02:00
Tom Hubrecht 40f34926bb Fix vagrant setup w/ daphne 2024-07-12 14:54:14 +02:00
thubrecht 69976a878a Merge pull request 'Envoi de mail lors de la création d'un trigramme' (#833) from agroudiev/mail-creation-trigramme into master
Reviewed-on: DGNum/gestioCOF#833
2024-07-06 16:54:25 +02:00
Antoine Groudiev 6621ae3950 style(kfet): suppression de return 2024-07-06 16:49:35 +02:00
Antoine Groudiev 9288daaf9e feat(kfet): envoi de mail lors de la création d'un trigramme 2024-07-06 12:14:49 +02:00
Tom Hubrecht e92c500940 feat(shell.nix): Switch to python 3.9 2024-02-11 19:59:29 +01:00
Tom Hubrecht d75eaf583f Merge branch 'master' into 'master'
Modification du délai pour l'indicateur K-Fêt ouverte

See merge request klub-dev-ens/gestioCOF!529
2023-12-10 10:11:33 +01:00
soyouzpanda 55bd3ab51d
Modification du délai du websocket 2023-12-08 21:17:53 +01:00
Tom Hubrecht f640a25f59 Merge branch 'petitcours-template-tweak' into 'master'
[petitcours] Tweak `eleve.txt` template

See merge request klub-dev-ens/gestioCOF!528
2023-10-03 15:20:40 +02:00
Leo Lanteri--Thauvin f881c7cd8b [petitcours] Tweak eleve.txt template 2023-09-11 16:44:45 +02:00
Tom Hubrecht b548b87c25 Version 0.15.1 et Changelog 2023-06-15 13:52:53 +02:00
Tom Hubrecht a72302291f Merge branch 'send_neg' into 'master'
feat(kfet): Change l'adresse utilisée pour envoyer les mails de négatif

See merge request klub-dev-ens/gestioCOF!526
2023-06-15 13:33:44 +02:00
Tom Hubrecht a0bde75f50 feat(kfet): Change l'adresse utilisée pour envoyer les mails de négatif 2023-06-15 13:03:54 +02:00
Tom Hubrecht 44b19c12e5 Merge branch 'send_neg' into 'master'
fix(kfet): Récupère lors d'une erreur due à smtplib

See merge request klub-dev-ens/gestioCOF!525
2023-06-15 10:48:23 +02:00
Tom Hubrecht f97d339a1c fix(kfet): Récupère lors d'une erreur due à smtplib 2023-06-14 20:56:25 +02:00
Tom Hubrecht 094116e88d Merge branch 'thubrecht/date-adhesion' into 'master'
Rajout de la date d'adhésion sur les profils COF

Closes #303

See merge request klub-dev-ens/gestioCOF!521
2023-05-26 09:31:45 +02:00
Tom Hubrecht b32a07fc22 Version 0.15 et mise à jour du Changelog 2023-05-22 20:42:23 +02:00
Tom Hubrecht 4fc9902cf6 Merge branch 'thubrecht/contact-soiree' into 'master'
feat(kfet): Ajout d'un formulaire de demande de soirée

See merge request klub-dev-ens/gestioCOF!523
2023-05-22 20:37:38 +02:00
Tom Hubrecht 7164cfa37a feat(kfet): Ajout d'un formulaire de demande de soirée 2023-05-22 20:30:05 +02:00
Tom Hubrecht 90f96fb5c9 Merge branch 'thubrecht/contact' into 'master'
feat(kfet): Ajoute un formulaire de contact

Closes #302

See merge request klub-dev-ens/gestioCOF!520
2023-05-22 19:06:26 +02:00
Tom Hubrecht e50249355d feat(kfet): Ajoute un formulaire de contact 2023-05-22 18:59:46 +02:00
Tom Hubrecht c304d734d9 Merge branch 'thubrecht/comptes-inactifs' into 'master'
feat(kfet): Désactive l'envoi des mails pour les comptes gelés

See merge request klub-dev-ens/gestioCOF!522
2023-05-22 18:34:51 +02:00
Tom Hubrecht c36dd30bce fix(kfet): Affiche la bonne information 2023-05-22 18:26:24 +02:00
Tom Hubrecht 2571cc955e feat(kfet): Désactive l'envoi des mails pour les comptes gelés
On utilise la fonctionnalité `is_frozen` pour marquer les comptes qui n'ont plus d'adresse valide, et on répare le formulaire de màj de compte.
2023-05-22 18:23:50 +02:00
Tom Hubrecht 3eaac5c68f feat(cof): Rajoute la date d'adhésion dans les profils 2023-05-22 11:28:23 +02:00
Tom Hubrecht af4c8e0744 Update shell.nix and use django-types 2023-05-22 10:57:11 +02:00
Tom Hubrecht 14e0a3ef0a Version 0.14 et mise à jour du changelog 2023-05-19 20:18:11 +02:00
Tom Hubrecht 83078d4726 Merge branch 'thubrecht/date-js' into 'master'
Thubrecht/date js

See merge request klub-dev-ens/gestioCOF!518
2023-05-19 17:34:27 +02:00
Tom Hubrecht cb262ad479 fix(kfet): Update timezone data for moment.js 2023-05-19 16:45:15 +02:00
102 changed files with 1089 additions and 419 deletions

View file

@ -43,13 +43,21 @@ variables:
# Keep this disabled for now, as it may kill GitLab... # Keep this disabled for now, as it may kill GitLab...
# coverage: '/TOTAL.*\s(\d+\.\d+)\%$/' # coverage: '/TOTAL.*\s(\d+\.\d+)\%$/'
kfettest:
stage: test
extends: .test_template
variables:
DJANGO_SETTINGS_MODULE: "gestioasso.settings.cof_prod"
script:
- coverage run manage.py test kfet
coftest: coftest:
stage: test stage: test
extends: .test_template extends: .test_template
variables: variables:
DJANGO_SETTINGS_MODULE: "gestioasso.settings.cof_prod" DJANGO_SETTINGS_MODULE: "gestioasso.settings.cof_prod"
script: script:
- coverage run manage.py test gestioncof bda kfet petitscours shared --parallel - coverage run manage.py test gestioncof bda petitscours shared --parallel
bdstest: bdstest:
stage: test stage: test

View file

@ -23,10 +23,36 @@ adhérents ni des cotisations.
## TODO Prod ## TODO Prod
- Lancer `python manage.py update_translation_fields` après la migration
- Mettre à jour les units systemd `daphne.service` et `worker.service`
- Créer un compte hCaptcha (https://www.hcaptcha.com/), au COF, et remplacer les secrets associés - Créer un compte hCaptcha (https://www.hcaptcha.com/), au COF, et remplacer les secrets associés
## Version ??? - ??/??/???? ## Version ??? - ??/??/????
## Version 0.15.1 - 15/06/2023
### K-Fêt
- Rattrape les erreurs d'envoi de mail de négatif
- Utilise l'adresse chefs pour les envois de négatifs
## Version 0.15 - 22/05/2023
### K-Fêt
- Rajoute un formulaire de contact
- Rajoute un formulaire de demande de soirée
- Désactive les mails d'envoi de négatifs sur les comptes gelés
## Version 0.14 - 19/05/2023
- Répare les dépendances en spécifiant toutes les versions
### K-Fêt
- Répare la gestion des changement d'heure via moment.js
## Version 0.13 - 19/02/2023 ## Version 0.13 - 19/02/2023
### K-Fêt ### K-Fêt
@ -42,6 +68,8 @@ adhérents ni des cotisations.
- Fixe un problème de rendu causé par l'agrandissement du menu - Fixe un problème de rendu causé par l'agrandissement du menu
- Mise à jour vers Channels 3.x et Django 3.2
## Version 0.12 - 17/06/2022 ## Version 0.12 - 17/06/2022
### K-Fêt ### K-Fêt

View file

@ -0,0 +1,23 @@
# Generated by Django 3.2.13 on 2022-06-30 10:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bda", "0018_auto_20201021_1818"),
]
operations = [
migrations.AlterUniqueTogether(
name="choixspectacle",
unique_together=set(),
),
migrations.AddConstraint(
model_name="choixspectacle",
constraint=models.UniqueConstraint(
fields=("participant", "spectacle"), name="unique_participation"
),
),
]

View file

@ -253,7 +253,11 @@ class ChoixSpectacle(models.Model):
class Meta: class Meta:
ordering = ("priority",) ordering = ("priority",)
unique_together = (("participant", "spectacle"),) constraints = [
models.UniqueConstraint(
fields=["participant", "spectacle"], name="unique_participation"
)
]
verbose_name = "voeu" verbose_name = "voeu"
verbose_name_plural = "voeux" verbose_name_plural = "voeux"

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load staticfiles %} {% load static %}
{% block extra_head %} {% block extra_head %}
<link type="text/css" rel="stylesheet" href="{% static "bda/css/bda.css" %}" /> <link type="text/css" rel="stylesheet" href="{% static "bda/css/bda.css" %}" />

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load staticfiles %} {% load static %}
{% block realcontent %} {% block realcontent %}
<h2>État des inscriptions BdA</h2> <h2>État des inscriptions BdA</h2>

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load staticfiles %} {% load static %}
{% block extra_head %} {% block extra_head %}
<script type="text/javascript" src="{% static 'vendor/jquery/jquery-ui.min.js' %}" ></script> <script type="text/javascript" src="{% static 'vendor/jquery/jquery-ui.min.js' %}" ></script>

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load staticfiles %} {% load static %}
{% block realcontent %} {% block realcontent %}
<h2>{{ spectacle }}</h2> <h2>{{ spectacle }}</h2>

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load staticfiles %} {% load static %}
{%block realcontent %} {%block realcontent %}

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load staticfiles %} {% load static %}
{% block realcontent %} {% block realcontent %}
<h2>Inscription à une revente</h2> <h2>Inscription à une revente</h2>

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load staticfiles %} {% load static %}
{% block realcontent %} {% block realcontent %}

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load staticfiles %} {% load static %}
{% block realcontent %} {% block realcontent %}

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load staticfiles%} {% load static %}
{% block realcontent %} {% block realcontent %}
<h2>Inscriptions pour BdA-Revente</h2> <h2>Inscriptions pour BdA-Revente</h2>

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load staticfiles %} {% load static %}
{% block realcontent %} {% block realcontent %}

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load staticfiles %} {% load static %}
{% block extra_head %} {% block extra_head %}
<link type="text/css" rel="stylesheet" href="{% static "bda/css/bda.css" %}" /> <link type="text/css" rel="stylesheet" href="{% static "bda/css/bda.css" %}" />

View file

@ -1,74 +1,80 @@
from django.conf.urls import url from django.urls import re_path
from bda import views from bda import views
from bda.views import SpectacleListView from bda.views import SpectacleListView
from gestioncof.decorators import buro_required from gestioncof.decorators import buro_required
urlpatterns = [ urlpatterns = [
url( re_path(
r"^inscription/(?P<tirage_id>\d+)$", r"^inscription/(?P<tirage_id>\d+)$",
views.inscription, views.inscription,
name="bda-tirage-inscription", name="bda-tirage-inscription",
), ),
url(r"^places/(?P<tirage_id>\d+)$", views.places, name="bda-places-attribuees"), re_path(r"^places/(?P<tirage_id>\d+)$", views.places, name="bda-places-attribuees"),
url(r"^etat-places/(?P<tirage_id>\d+)$", views.etat_places, name="bda-etat-places"), re_path(
url(r"^tirage/(?P<tirage_id>\d+)$", views.tirage, name="bda-tirage"), r"^etat-places/(?P<tirage_id>\d+)$", views.etat_places, name="bda-etat-places"
url( ),
re_path(r"^tirage/(?P<tirage_id>\d+)$", views.tirage, name="bda-tirage"),
re_path(
r"^spectacles/(?P<tirage_id>\d+)$", r"^spectacles/(?P<tirage_id>\d+)$",
buro_required(SpectacleListView.as_view()), buro_required(SpectacleListView.as_view()),
name="bda-liste-spectacles", name="bda-liste-spectacles",
), ),
url( re_path(
r"^spectacles/(?P<tirage_id>\d+)/(?P<spectacle_id>\d+)$", r"^spectacles/(?P<tirage_id>\d+)/(?P<spectacle_id>\d+)$",
views.spectacle, views.spectacle,
name="bda-spectacle", name="bda-spectacle",
), ),
url( re_path(
r"^spectacles/unpaid/(?P<tirage_id>\d+)$", r"^spectacles/unpaid/(?P<tirage_id>\d+)$",
views.UnpaidParticipants.as_view(), views.UnpaidParticipants.as_view(),
name="bda-unpaid", name="bda-unpaid",
), ),
url( re_path(
r"^spectacles/autocomplete$", r"^spectacles/autocomplete$",
views.spectacle_autocomplete, views.spectacle_autocomplete,
name="bda-spectacle-autocomplete", name="bda-spectacle-autocomplete",
), ),
url( re_path(
r"^participants/autocomplete$", r"^participants/autocomplete$",
views.participant_autocomplete, views.participant_autocomplete,
name="bda-participant-autocomplete", name="bda-participant-autocomplete",
), ),
# Urls BdA-Revente # Urls BdA-Revente
url( re_path(
r"^revente/(?P<tirage_id>\d+)/manage$", r"^revente/(?P<tirage_id>\d+)/manage$",
views.revente_manage, views.revente_manage,
name="bda-revente-manage", name="bda-revente-manage",
), ),
url( re_path(
r"^revente/(?P<tirage_id>\d+)/subscribe$", r"^revente/(?P<tirage_id>\d+)/subscribe$",
views.revente_subscribe, views.revente_subscribe,
name="bda-revente-subscribe", name="bda-revente-subscribe",
), ),
url( re_path(
r"^revente/(?P<tirage_id>\d+)/tirages$", r"^revente/(?P<tirage_id>\d+)/tirages$",
views.revente_tirages, views.revente_tirages,
name="bda-revente-tirages", name="bda-revente-tirages",
), ),
url( re_path(
r"^revente/(?P<spectacle_id>\d+)/buy$", r"^revente/(?P<spectacle_id>\d+)/buy$",
views.revente_buy, views.revente_buy,
name="bda-revente-buy", name="bda-revente-buy",
), ),
url( re_path(
r"^revente/(?P<revente_id>\d+)/confirm$", r"^revente/(?P<revente_id>\d+)/confirm$",
views.revente_confirm, views.revente_confirm,
name="bda-revente-confirm", name="bda-revente-confirm",
), ),
url( re_path(
r"^revente/(?P<tirage_id>\d+)/shotgun$", r"^revente/(?P<tirage_id>\d+)/shotgun$",
views.revente_shotgun, views.revente_shotgun,
name="bda-revente-shotgun", name="bda-revente-shotgun",
), ),
url(r"^mails-rappel/(?P<spectacle_id>\d+)$", views.send_rappel, name="bda-rappels"), re_path(
url(r"^catalogue/(?P<request_type>[a-z]+)$", views.catalogue, name="bda-catalogue"), r"^mails-rappel/(?P<spectacle_id>\d+)$", views.send_rappel, name="bda-rappels"
),
re_path(
r"^catalogue/(?P<request_type>[a-z]+)$", views.catalogue, name="bda-catalogue"
),
] ]

View file

@ -1 +0,0 @@
default_app_config = "bds.apps.BdsConfig"

View file

@ -1,5 +1,4 @@
from django import apps as global_apps from django.apps import AppConfig, apps as global_apps
from django.apps import AppConfig
from django.db.models import Q from django.db.models import Q
from django.db.models.signals import post_migrate from django.db.models.signals import post_migrate

View file

@ -1,4 +1,4 @@
{% load staticfiles %} {% load static %}
{% load bulma_utils %} {% load bulma_utils %}
<!DOCTYPE html> <!DOCTYPE html>

View file

@ -31,7 +31,7 @@ class TestHomeView(TestCase):
user, backend="django.contrib.auth.backends.ModelBackend" user, backend="django.contrib.auth.backends.ModelBackend"
) )
resp = self.client.get(reverse("bds:home")) resp = self.client.get(reverse("bds:home"))
self.assertEquals(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
class TestRegistrationView(TestCase): class TestRegistrationView(TestCase):
@ -48,12 +48,12 @@ class TestRegistrationView(TestCase):
# Logged-in but unprivileged GET # Logged-in but unprivileged GET
client.force_login(user, backend="django.contrib.auth.backends.ModelBackend") client.force_login(user, backend="django.contrib.auth.backends.ModelBackend")
resp = client.get(url) resp = client.get(url)
self.assertEquals(resp.status_code, 403) self.assertEqual(resp.status_code, 403)
# Burô user GET # Burô user GET
give_bds_buro_permissions(user) give_bds_buro_permissions(user)
resp = client.get(url) resp = client.get(url)
self.assertEquals(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
@mock.patch("gestioncof.signals.messages") @mock.patch("gestioncof.signals.messages")
def test_get(self, mock_messages): def test_get(self, mock_messages):
@ -68,9 +68,9 @@ class TestRegistrationView(TestCase):
# Logged-in but unprivileged GET # Logged-in but unprivileged GET
client.force_login(user, backend="django.contrib.auth.backends.ModelBackend") client.force_login(user, backend="django.contrib.auth.backends.ModelBackend")
resp = client.get(url) resp = client.get(url)
self.assertEquals(resp.status_code, 403) self.assertEqual(resp.status_code, 403)
# Burô user GET # Burô user GET
give_bds_buro_permissions(user) give_bds_buro_permissions(user)
resp = client.get(url) resp = client.get(url)
self.assertEquals(resp.status_code, 200) self.assertEqual(resp.status_code, 200)

View file

@ -0,0 +1,63 @@
# Generated by Django 3.2.13 on 2022-06-30 10:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("events", "0004_unique_constraints"),
]
operations = [
migrations.AlterUniqueTogether(
name="extrafield",
unique_together=set(),
),
migrations.AlterUniqueTogether(
name="extrafieldcontent",
unique_together=set(),
),
migrations.AlterUniqueTogether(
name="option",
unique_together=set(),
),
migrations.AlterUniqueTogether(
name="optionchoice",
unique_together=set(),
),
migrations.AlterUniqueTogether(
name="registration",
unique_together=set(),
),
migrations.AddConstraint(
model_name="extrafield",
constraint=models.UniqueConstraint(
fields=("event", "name"), name="unique_extra_field"
),
),
migrations.AddConstraint(
model_name="extrafieldcontent",
constraint=models.UniqueConstraint(
fields=("field", "registration"), name="unique_extra_field_content"
),
),
migrations.AddConstraint(
model_name="option",
constraint=models.UniqueConstraint(
fields=("event", "name"), name="unique_event_option"
),
),
migrations.AddConstraint(
model_name="optionchoice",
constraint=models.UniqueConstraint(
fields=("option", "choice"), name="unique_option_choice"
),
),
migrations.AddConstraint(
model_name="registration",
constraint=models.UniqueConstraint(
fields=("event", "user"), name="unique_registration"
),
),
]

View file

@ -72,9 +72,13 @@ class Option(models.Model):
multi_choices = models.BooleanField(_("choix multiples"), default=False) multi_choices = models.BooleanField(_("choix multiples"), default=False)
class Meta: class Meta:
constraints = [
models.UniqueConstraint(
fields=["event", "name"], name="unique_event_option"
)
]
verbose_name = _("option d'événement") verbose_name = _("option d'événement")
verbose_name_plural = _("options d'événement") verbose_name_plural = _("options d'événement")
unique_together = [["event", "name"]]
def __str__(self): def __str__(self):
return self.name return self.name
@ -87,9 +91,13 @@ class OptionChoice(models.Model):
choice = models.CharField(_("choix"), max_length=200) choice = models.CharField(_("choix"), max_length=200)
class Meta: class Meta:
constraints = [
models.UniqueConstraint(
fields=["option", "choice"], name="unique_option_choice"
)
]
verbose_name = _("choix d'option d'événement") verbose_name = _("choix d'option d'événement")
verbose_name_plural = _("choix d'option d'événement") verbose_name_plural = _("choix d'option d'événement")
unique_together = [["option", "choice"]]
def __str__(self): def __str__(self):
return self.choice return self.choice
@ -118,7 +126,9 @@ class ExtraField(models.Model):
field_type = models.CharField(_("type de champ"), max_length=9, choices=FIELD_TYPE) field_type = models.CharField(_("type de champ"), max_length=9, choices=FIELD_TYPE)
class Meta: class Meta:
unique_together = [["event", "name"]] constraints = [
models.UniqueConstraint(fields=["event", "name"], name="unique_extra_field")
]
class ExtraFieldContent(models.Model): class ExtraFieldContent(models.Model):
@ -137,9 +147,13 @@ class ExtraFieldContent(models.Model):
) )
class Meta: class Meta:
constraints = [
models.UniqueConstraint(
fields=["field", "registration"], name="unique_extra_field_content"
)
]
verbose_name = _("contenu d'un champ événement supplémentaire") verbose_name = _("contenu d'un champ événement supplémentaire")
verbose_name_plural = _("contenus d'un champ événement supplémentaire") verbose_name_plural = _("contenus d'un champ événement supplémentaire")
unique_together = [["field", "registration"]]
def __str__(self): def __str__(self):
max_length = 50 max_length = 50
@ -163,9 +177,13 @@ class Registration(models.Model):
) )
class Meta: class Meta:
constraints = [
models.UniqueConstraint(
fields=["event", "user"], name="unique_registration"
)
]
verbose_name = _("inscription à un événement") verbose_name = _("inscription à un événement")
verbose_name_plural = _("inscriptions à un événement") verbose_name_plural = _("inscriptions à un événement")
unique_together = [["event", "user"]]
def __str__(self): def __str__(self):
return "inscription de {} à {}".format(self.user, self.event) return "inscription de {} à {}".format(self.user, self.event)

View file

@ -1,8 +1,15 @@
"""
ASGI entrypoint. Configures Django and then runs the application
defined in the ASGI_APPLICATION setting.
"""
import os import os
from channels.asgi import get_channel_layer import django
from channels.routing import get_default_application
if "DJANGO_SETTINGS_MODULE" not in os.environ: if "DJANGO_SETTINGS_MODULE" not in os.environ:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestioasso.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestioasso.settings.local")
channel_layer = get_channel_layer() django.setup()
application = get_default_application()

View file

@ -1,3 +1,20 @@
from channels.routing import include from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from django.urls import path
routing = [include("kfet.routing.routing", path=r"^/ws/k-fet")] from kfet.routing import KFRouter
application = ProtocolTypeRouter(
{
# WebSocket chat handler
"websocket": AuthMiddlewareStack(
URLRouter(
[
path("ws/k-fet", KFRouter),
]
)
),
"http": get_asgi_application(),
}
)

View file

@ -85,7 +85,6 @@ MIDDLEWARE = (
+ MIDDLEWARE + MIDDLEWARE
+ [ + [
"djconfig.middleware.DjConfigMiddleware", "djconfig.middleware.DjConfigMiddleware",
"wagtail.core.middleware.SiteMiddleware",
"wagtail.contrib.redirects.middleware.RedirectMiddleware", "wagtail.contrib.redirects.middleware.RedirectMiddleware",
] ]
) )
@ -109,6 +108,8 @@ MEDIA_URL = "/gestion/media/"
CORS_ORIGIN_WHITELIST = ("bda.ens.fr", "www.bda.ens.fr" "cof.ens.fr", "www.cof.ens.fr") CORS_ORIGIN_WHITELIST = ("bda.ens.fr", "www.bda.ens.fr" "cof.ens.fr", "www.cof.ens.fr")
ASGI_APPLICATION = "gestioasso.routing.application"
# --- # ---
# Auth-related stuff # Auth-related stuff
# --- # ---
@ -147,7 +148,7 @@ CACHES = {
CHANNEL_LAYERS = { CHANNEL_LAYERS = {
"default": { "default": {
"BACKEND": "asgi_redis.RedisChannelLayer", "BACKEND": "shared.channels.ChannelLayer",
"CONFIG": { "CONFIG": {
"hosts": [ "hosts": [
( (
@ -160,11 +161,9 @@ CHANNEL_LAYERS = {
) )
] ]
}, },
"ROUTING": "gestioasso.routing.routing",
} }
} }
# --- # ---
# reCAPTCHA settings # reCAPTCHA settings
# https://github.com/praekelt/django-recaptcha # https://github.com/praekelt/django-recaptcha
@ -206,8 +205,9 @@ MAIL_DATA = {
"REPLYTO": "cof@ens.fr", "REPLYTO": "cof@ens.fr",
}, },
"rappels": {"FROM": "Le BdA <bda@ens.fr>", "REPLYTO": "Le BdA <bda@ens.fr>"}, "rappels": {"FROM": "Le BdA <bda@ens.fr>", "REPLYTO": "Le BdA <bda@ens.fr>"},
"rappel_negatif": { "kfet": {
"FROM": "La K-Fêt <k-fet@ens.fr>", "FROM": "La K-Fêt <chefs-k-fet@ens.fr>",
"REPLYTO": "La K-Fêt <chefs-k-fet@ens.fr>",
}, },
"revente": { "revente": {
"FROM": "BdA-Revente <bda-revente@ens.fr>", "FROM": "BdA-Revente <bda-revente@ens.fr>",

View file

@ -101,7 +101,7 @@ TEMPLATES = [
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.postgresql_psycopg2", "ENGINE": "django.db.backends.postgresql",
"NAME": DBNAME, "NAME": DBNAME,
"USER": DBUSER, "USER": DBUSER,
"PASSWORD": DBPASSWD, "PASSWORD": DBPASSWD,
@ -111,6 +111,7 @@ DATABASES = {
SITE_ID = 1 SITE_ID = 1
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# --- # ---
# Internationalization # Internationalization

View file

@ -47,8 +47,7 @@ CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"
# Use the default in memory asgi backend for local development # Use the default in memory asgi backend for local development
CHANNEL_LAYERS = { CHANNEL_LAYERS = {
"default": { "default": {
"BACKEND": "asgiref.inmemory.ChannelLayer", "BACKEND": "channels.layers.InMemoryChannelLayer",
"ROUTING": "gestioasso.routing.routing",
} }
} }

View file

@ -1 +0,0 @@
default_app_config = "gestioncof.apps.GestioncofConfig"

View file

@ -6,7 +6,7 @@ from django.contrib.auth.models import Group, Permission, User
from django.db.models import Q from django.db.models import Q
from django.urls import reverse from django.urls import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from gestioncof.models import ( from gestioncof.models import (
Club, Club,

View file

@ -1 +0,0 @@
default_app_config = "gestioncof.cms.apps.COFCMSAppConfig"

View file

@ -2,7 +2,7 @@ from datetime import date, timedelta
from django import template from django import template
from django.utils import formats, timezone from django.utils import formats, timezone
from django.utils.translation import ugettext as _ from django.utils.translation import gettext as _
from ..models import COFActuPage, COFRootPage from ..models import COFActuPage, COFRootPage

View file

@ -3,7 +3,7 @@ from django.contrib.auth import get_user_model
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import AuthenticationForm
from django.forms.formsets import BaseFormSet, formset_factory from django.forms.formsets import BaseFormSet, formset_factory
from django.forms.widgets import CheckboxSelectMultiple, RadioSelect from django.forms.widgets import CheckboxSelectMultiple, RadioSelect
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from djconfig.forms import ConfigForm from djconfig.forms import ConfigForm
from bda.models import Spectacle from bda.models import Spectacle
@ -276,7 +276,9 @@ class RegistrationProfileForm(forms.ModelForm):
self.fields["mailing_bda_revente"].initial = True self.fields["mailing_bda_revente"].initial = True
self.fields["mailing_unernestaparis"].initial = True self.fields["mailing_unernestaparis"].initial = True
self.fields.keyOrder = [ class Meta:
model = CofProfile
fields = [
"login_clipper", "login_clipper",
"phone", "phone",
"occupation", "occupation",
@ -290,22 +292,6 @@ class RegistrationProfileForm(forms.ModelForm):
"comments", "comments",
] ]
class Meta:
model = CofProfile
fields = (
"login_clipper",
"phone",
"occupation",
"departement",
"is_cof",
"type_cotiz",
"mailing_cof",
"mailing_bda",
"mailing_bda_revente",
"mailing_unernestaparis",
"comments",
)
STATUS_CHOICES = ( STATUS_CHOICES = (
("no", "Non"), ("no", "Non"),

View file

@ -0,0 +1,43 @@
# Generated by Django 3.2.13 on 2022-06-30 10:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("gestioncof", "0018_petitscours_email"),
]
operations = [
migrations.AlterUniqueTogether(
name="eventregistration",
unique_together=set(),
),
migrations.AlterUniqueTogether(
name="petitcoursability",
unique_together=set(),
),
migrations.AlterUniqueTogether(
name="surveyanswer",
unique_together=set(),
),
migrations.AddConstraint(
model_name="eventregistration",
constraint=models.UniqueConstraint(
fields=("user", "event"), name="unique_event_registration"
),
),
migrations.AddConstraint(
model_name="petitcoursability",
constraint=models.UniqueConstraint(
fields=("user", "niveau", "matiere"), name="unique_competence_level"
),
),
migrations.AddConstraint(
model_name="surveyanswer",
constraint=models.UniqueConstraint(
fields=("user", "survey"), name="unique_survey_answer"
),
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 2.2.28 on 2023-05-22 09:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("gestioncof", "0018_petitscours_email"),
]
operations = [
migrations.AddField(
model_name="cofprofile",
name="date_adhesion",
field=models.DateField(
blank=True, null=True, verbose_name="Date d'adhésion"
),
),
]

View file

@ -2,7 +2,7 @@ from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.db.models.signals import post_delete, post_save from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from bda.models import Spectacle from bda.models import Spectacle
from shared.utils import choices_length from shared.utils import choices_length
@ -50,6 +50,7 @@ class CofProfile(models.Model):
"Login clipper", max_length=32, blank=True, unique=True, null=True "Login clipper", max_length=32, blank=True, unique=True, null=True
) )
is_cof = models.BooleanField("Membre du COF", default=False) is_cof = models.BooleanField("Membre du COF", default=False)
date_adhesion = models.DateField("Date d'adhésion", blank=True, null=True)
phone = models.CharField("Téléphone", max_length=20, blank=True) phone = models.CharField("Téléphone", max_length=20, blank=True)
occupation = models.CharField( occupation = models.CharField(
_("Occupation"), _("Occupation"),
@ -193,8 +194,12 @@ class EventRegistration(models.Model):
paid = models.BooleanField("A payé", default=False) paid = models.BooleanField("A payé", default=False)
class Meta: class Meta:
constraints = [
models.UniqueConstraint(
fields=["user", "event"], name="unique_event_registration"
)
]
verbose_name = "Inscription" verbose_name = "Inscription"
unique_together = ("user", "event")
def __str__(self): def __str__(self):
return "Inscription de {} à {}".format(self.user, self.event.title) return "Inscription de {} à {}".format(self.user, self.event.title)
@ -246,8 +251,12 @@ class SurveyAnswer(models.Model):
answers = models.ManyToManyField(SurveyQuestionAnswer, related_name="selected_by") answers = models.ManyToManyField(SurveyQuestionAnswer, related_name="selected_by")
class Meta: class Meta:
constraints = [
models.UniqueConstraint(
fields=["user", "survey"], name="unique_survey_answer"
)
]
verbose_name = "Réponses" verbose_name = "Réponses"
unique_together = ("user", "survey")
def __str__(self): def __str__(self):
return "Réponse de %s sondage %s" % ( return "Réponse de %s sondage %s" % (

View file

@ -1,7 +1,7 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.signals import user_logged_in from django.contrib.auth.signals import user_logged_in
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_cas_ng.signals import cas_user_authenticated from django_cas_ng.signals import cas_user_authenticated

View file

@ -1,4 +1,4 @@
{% load staticfiles %} {% load static %}
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="fr"> <html xmlns="http://www.w3.org/1999/xhtml" lang="fr">

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load staticfiles %} {% load static %}
{% block page_size %}col-sm-8{% endblock %} {% block page_size %}col-sm-8{% endblock %}

View file

@ -1,4 +1,4 @@
{% load staticfiles %} {% load static %}
<script type="text/javascript"> <script type="text/javascript">
var supernifty_tristate = function() { var supernifty_tristate = function() {
var var

View file

@ -1,5 +1,5 @@
import uuid import uuid
from datetime import timedelta from datetime import date, timedelta
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
@ -194,7 +194,9 @@ class RegistrationViewTests(ViewTestCaseMixin, TestCase):
) )
er = e.eventregistration_set.get(user=self.users["user"]) er = e.eventregistration_set.get(user=self.users["user"])
self.assertQuerysetEqual(er.options.all(), map(repr, [oc1, oc3]), ordered=False) self.assertQuerysetEqual(
er.options.all(), map(repr, [oc1, oc3]), transform=repr, ordered=False
)
self.assertCountEqual( self.assertCountEqual(
er.comments.values_list("content", flat=True), ["comment 1"] er.comments.values_list("content", flat=True), ["comment 1"]
) )
@ -299,10 +301,10 @@ class RegistrationAutocompleteViewTests(MockLDAPMixin, ViewTestCaseMixin, TestCa
raise ValueError("Unexpected section name: {}".format(section.name)) raise ValueError("Unexpected section name: {}".format(section.name))
self.assertQuerysetEqual( self.assertQuerysetEqual(
others, map(str, expected_others), ordered=False, transform=str others, map(str, expected_others), transform=str, ordered=False
) )
self.assertQuerysetEqual( self.assertQuerysetEqual(
members, map(str, expected_members), ordered=False, transform=str members, map(str, expected_members), transform=str, ordered=False
) )
self.assertSetEqual( self.assertSetEqual(
set(clippers), set(map(LDAPSearch().result_verbose_name, expected_clippers)) set(clippers), set(map(LDAPSearch().result_verbose_name, expected_clippers))
@ -484,6 +486,7 @@ class ExportMembersViewTests(CSVResponseMixin, ViewTestCaseMixin, TestCase):
u1.last_name = "last" u1.last_name = "last"
u1.email = "user@mail.net" u1.email = "user@mail.net"
u1.save() u1.save()
u1.profile.date_adhesion = date(2023, 5, 22)
u1.profile.phone = "0123456789" u1.profile.phone = "0123456789"
u1.profile.departement = "Dept" u1.profile.departement = "Dept"
u1.profile.save() u1.profile.save()
@ -505,8 +508,9 @@ class ExportMembersViewTests(CSVResponseMixin, ViewTestCaseMixin, TestCase):
"1A", "1A",
"Dept", "Dept",
"normalien", "normalien",
"2023-05-22",
], ],
[str(u2.pk), "staff", "", "", "", "", "1A", "", "normalien"], [str(u2.pk), "staff", "", "", "", "", "1A", "", "normalien", "None"],
], ],
) )
@ -646,7 +650,10 @@ class ClubListViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertQuerysetEqual( self.assertQuerysetEqual(
r.context["owned_clubs"], map(repr, [self.c1, self.c2]), ordered=False r.context["owned_clubs"],
map(repr, [self.c1, self.c2]),
transform=repr,
ordered=False,
) )
@ -948,7 +955,10 @@ class EventViewTests(ViewTestCaseMixin, TestCase):
er = self.e.eventregistration_set.get(user=self.users["user"]) er = self.e.eventregistration_set.get(user=self.users["user"])
self.assertQuerysetEqual( self.assertQuerysetEqual(
er.options.all(), map(repr, [self.oc1, self.oc3, self.oc4]), ordered=False er.options.all(),
map(repr, [self.oc1, self.oc3, self.oc4]),
transform=repr,
ordered=False,
) )
# TODO: Make the view care about comments. # TODO: Make the view care about comments.
# self.assertQuerysetEqual( # self.assertQuerysetEqual(
@ -973,7 +983,9 @@ class EventViewTests(ViewTestCaseMixin, TestCase):
self.assertIn(self.post_expected_message, get_messages(r.wsgi_request)) self.assertIn(self.post_expected_message, get_messages(r.wsgi_request))
er.refresh_from_db() er.refresh_from_db()
self.assertQuerysetEqual(er.options.all(), map(repr, [self.oc3]), ordered=False) self.assertQuerysetEqual(
er.options.all(), map(repr, [self.oc3]), transform=repr, ordered=False
)
# TODO: Make the view care about comments. # TODO: Make the view care about comments.
# self.assertQuerysetEqual( # self.assertQuerysetEqual(
# er.comments.all(), map(repr, []), # er.comments.all(), map(repr, []),
@ -1027,7 +1039,10 @@ class EventStatusViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertQuerysetEqual( self.assertQuerysetEqual(
r.context["user_choices"], map(repr, expected), ordered=False r.context["user_choices"],
map(repr, expected),
transform=repr,
ordered=False,
) )
def test_filter_none(self): def test_filter_none(self):
@ -1094,7 +1109,10 @@ class SurveyViewTests(ViewTestCaseMixin, TestCase):
a = self.s.surveyanswer_set.get(user=self.users["user"]) a = self.s.surveyanswer_set.get(user=self.users["user"])
self.assertQuerysetEqual( self.assertQuerysetEqual(
a.answers.all(), map(repr, [self.qa1, self.qa3, self.qa4]), ordered=False a.answers.all(),
map(repr, [self.qa1, self.qa3, self.qa4]),
transform=repr,
ordered=False,
) )
def test_post_edit(self): def test_post_edit(self):
@ -1113,7 +1131,9 @@ class SurveyViewTests(ViewTestCaseMixin, TestCase):
self.assertIn(self.post_expected_message, get_messages(r.wsgi_request)) self.assertIn(self.post_expected_message, get_messages(r.wsgi_request))
a.refresh_from_db() a.refresh_from_db()
self.assertQuerysetEqual(a.answers.all(), map(repr, [self.qa3]), ordered=False) self.assertQuerysetEqual(
a.answers.all(), map(repr, [self.qa3]), transform=repr, ordered=False
)
def test_post_delete(self): def test_post_delete(self):
a = self.s.surveyanswer_set.create(user=self.users["user"]) a = self.s.surveyanswer_set.create(user=self.users["user"])
@ -1194,7 +1214,10 @@ class SurveyStatusViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertQuerysetEqual( self.assertQuerysetEqual(
r.context["user_answers"], map(repr, expected), ordered=False r.context["user_answers"],
map(repr, expected),
transform=repr,
ordered=False,
) )
def test_filter_none(self): def test_filter_none(self):

View file

@ -1,6 +1,6 @@
import csv import csv
import uuid import uuid
from datetime import timedelta from datetime import date, timedelta
from smtplib import SMTPRecipientsRefused from smtplib import SMTPRecipientsRefused
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
@ -20,7 +20,7 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.template import loader from django.template import loader
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView, TemplateView from django.views.generic import FormView, TemplateView
from django_cas_ng.views import LogoutView as CasLogoutView from django_cas_ng.views import LogoutView as CasLogoutView
from icalendar import Calendar, Event as Vevent from icalendar import Calendar, Event as Vevent
@ -86,6 +86,7 @@ class ResetComptes(BuroRequiredMixin, TemplateView):
nb_adherents = CofProfile.objects.filter(is_cof=True).count() nb_adherents = CofProfile.objects.filter(is_cof=True).count()
CofProfile.objects.update( CofProfile.objects.update(
is_cof=False, is_cof=False,
date_adhesion=None,
mailing_cof=False, mailing_cof=False,
mailing_bda=False, mailing_bda=False,
mailing_bda_revente=False, mailing_bda_revente=False,
@ -575,6 +576,9 @@ def registration(request):
profile = profile_form.save() profile = profile_form.save()
if profile.is_cof and not was_cof: if profile.is_cof and not was_cof:
notify_new_member(request, member) notify_new_member(request, member)
profile.date_adhesion = date.today()
profile.save()
# Enregistrement des inscriptions aux événements # Enregistrement des inscriptions aux événements
for form in event_formset: for form in event_formset:
if "status" not in form.cleaned_data: if "status" not in form.cleaned_data:
@ -715,6 +719,7 @@ def export_members(request):
profile.occupation, profile.occupation,
profile.departement, profile.departement,
profile.type_cotiz, profile.type_cotiz,
profile.date_adhesion,
] ]
writer.writerow([str(bit) for bit in bits]) writer.writerow([str(bit) for bit in bits])

View file

@ -1,3 +1,2 @@
default_app_config = "kfet.apps.KFetConfig"
KFET_DELETED_TRIGRAMME = "☠☠☠" KFET_DELETED_TRIGRAMME = "☠☠☠"
KFET_DELETED_USERNAME = "kfet_deleted_user" KFET_DELETED_USERNAME = "kfet_deleted_user"

View file

@ -1,4 +1,2 @@
default_app_config = "kfet.auth.apps.KFetAuthConfig"
KFET_GENERIC_USERNAME = "kfet_genericteam" KFET_GENERIC_USERNAME = "kfet_genericteam"
KFET_GENERIC_TRIGRAMME = "GNR" KFET_GENERIC_TRIGRAMME = "GNR"

View file

@ -1,6 +1,6 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.db.models.signals import post_migrate from django.db.models.signals import post_migrate
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
class KFetAuthConfig(AppConfig): class KFetAuthConfig(AppConfig):

View file

@ -1,5 +1,5 @@
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from shared.forms import ProtectedModelForm from shared.forms import ProtectedModelForm

View file

@ -1,7 +1,7 @@
from django.contrib.auth.models import Group, Permission from django.contrib.auth.models import Group, Permission
from django.db import models from django.db import models
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
KFET_APP_LABELS = ["kfet", "kfetauth"] KFET_APP_LABELS = ["kfet", "kfetauth"]

View file

@ -3,7 +3,7 @@ from django.contrib.auth.signals import user_logged_in
from django.dispatch import receiver from django.dispatch import receiver
from django.urls import reverse from django.urls import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _ from django.utils.translation import gettext_lazy as _
from .utils import get_kfet_generic_user from .utils import get_kfet_generic_user

View file

@ -40,6 +40,7 @@ class UserGroupFormTests(TestCase):
self.assertQuerysetEqual( self.assertQuerysetEqual(
groups_field.queryset, groups_field.queryset,
[repr(g.group_ptr) for g in self.kfet_groups], [repr(g.group_ptr) for g in self.kfet_groups],
transform=repr,
ordered=False, ordered=False,
) )

View file

@ -9,7 +9,7 @@ from django.http import QueryDict
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.views.generic import View from django.views.generic import View
from django.views.generic.edit import CreateView, UpdateView from django.views.generic.edit import CreateView, UpdateView

View file

@ -1 +0,0 @@
default_app_config = "kfet.cms.apps.KFetCMSAppConfig"

View file

@ -1,4 +1,4 @@
from django.contrib.staticfiles.templatetags.staticfiles import static from django.templatetags.static import static
from django.utils.html import format_html from django.utils.html import format_html
from wagtail.core import hooks from wagtail.core import hooks

View file

@ -1,5 +1,5 @@
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from wagtail.admin.edit_handlers import ( from wagtail.admin.edit_handlers import (
FieldPanel, FieldPanel,
FieldRowPanel, FieldRowPanel,

View file

@ -1,6 +1,18 @@
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from .utils import DjangoJsonWebsocketConsumer, PermConsumerMixin from .utils import DjangoJsonWebsocketConsumer, PermConsumerMixin
class KPsul(PermConsumerMixin, DjangoJsonWebsocketConsumer): class KPsul(PermConsumerMixin, DjangoJsonWebsocketConsumer):
groups = ["kfet.kpsul"] groups = ["kfet.kpsul"]
perms_connect = ["kfet.is_team"] perms_connect = ["kfet.is_team"]
async def kpsul(self, event):
await self.send_json(event)
@classmethod
@async_to_sync
async def group_send(cls, group, data):
channel_layer = get_channel_layer()
await channel_layer.group_send(group, data)

View file

@ -8,7 +8,7 @@ from django.core import validators
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.forms import modelformset_factory from django.forms import modelformset_factory
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from djconfig.forms import ConfigForm from djconfig.forms import ConfigForm
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
@ -45,6 +45,47 @@ class DateTimeWidget(forms.DateTimeInput):
js = ("kfet/vendor/bootstrap/bootstrap-datetimepicker.min.js",) js = ("kfet/vendor/bootstrap/bootstrap-datetimepicker.min.js",)
class ContactForm(forms.Form):
from_email = forms.EmailField(
label="Adresse mail",
help_text="Si aucune adresse mail n'est renseignée, la soumission sera anonyme.",
required=False,
)
subject = forms.CharField(label="Objet", required=True)
message = forms.CharField(widget=forms.Textarea, required=True)
def clean_from_email(self):
return self.cleaned_data["from_email"] or "Anonyme <k-fet@ens.psl.eu>"
class DemandeSoireeForm(forms.Form):
HORAIRE_CHOICES = map(lambda s: (s, s), ("22h", "23h", "00h", "01h", "02h", "03h"))
SERVICE_CHOICES = (
("K-Fêt", "K-Fêt standard (L'équipe K-Fêt fait le service normal au bar)"),
("Kalô", "Type Kalô (Vous ramenez vos propres boissons et servez vous-mêmes)"),
)
nom = forms.CharField()
from_email = forms.EmailField(label="Adresse mail de contact")
contact_boum = forms.BooleanField(label="Contacter le Boum", required=False)
contact_pls = forms.BooleanField(label="Contacter PLS", required=False)
theme = forms.CharField(label="Thème de la soirée")
horaire_fin = forms.ChoiceField(label="Horaire de fin", choices=HORAIRE_CHOICES)
service = forms.ChoiceField(label="Mode de service", choices=SERVICE_CHOICES)
date = forms.CharField(label="Date souhaitée")
respo1 = forms.CharField(label="Nom de la personne respo n°1")
respo2 = forms.CharField(label="Nom de la personne respo n°2")
respo3 = forms.CharField(label="Nom de la personne respo n°3")
respo4 = forms.CharField(label="Nom de la personne respo n°4")
remarques = forms.CharField(
label="Remarques supplémentaires", widget=forms.Textarea
)
# ----- # -----
# Account forms # Account forms
# ----- # -----

View file

@ -2,6 +2,7 @@
Gestion en ligne de commande des mails de rappel K-Fet. Gestion en ligne de commande des mails de rappel K-Fet.
""" """
import smtplib
from datetime import timedelta from datetime import timedelta
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
@ -10,6 +11,14 @@ from django.utils import timezone
from kfet.models import AccountNegative from kfet.models import AccountNegative
def send_mail(neg: AccountNegative, stdout) -> None:
try:
neg.send_rappel()
stdout.write(f"Mail de rappel pour {neg.account} envoyé avec succès.")
except smtplib.SMTPException:
stdout.write(f"Erreur lors de l'envoi du mail de rappel pour {neg.account}.")
class Command(BaseCommand): class Command(BaseCommand):
help = ( help = (
"Envoie un mail de rappel aux personnes en négatif.\n" "Envoie un mail de rappel aux personnes en négatif.\n"
@ -26,8 +35,9 @@ class Command(BaseCommand):
# On n'envoie des mails qu'aux comptes qui ont un négatif vraiment actif # On n'envoie des mails qu'aux comptes qui ont un négatif vraiment actif
# et dont la balance est négative # et dont la balance est négative
# On ignore les comptes gelés qui signinfient une adresse mail plus valide
account_negatives = AccountNegative.objects.filter( account_negatives = AccountNegative.objects.filter(
account__balance__lt=0 account__balance__lt=0, account__is_frozen=False
).exclude(end__lte=now) ).exclude(end__lte=now)
accounts_first_mail = account_negatives.filter( accounts_first_mail = account_negatives.filter(
@ -38,12 +48,10 @@ class Command(BaseCommand):
) )
for neg in accounts_first_mail: for neg in accounts_first_mail:
neg.send_rappel() send_mail(neg, self.stdout)
self.stdout.write(f"Mail de rappel pour {neg.account} envoyé avec succès.")
for neg in accounts_periodic_mail: for neg in accounts_periodic_mail:
neg.send_rappel() send_mail(neg, self.stdout)
self.stdout.write("Mail de rappel pour {neg.account} envoyé avec succès.")
if not (accounts_first_mail.exists() or accounts_periodic_mail.exists()): if not (accounts_first_mail.exists() or accounts_periodic_mail.exists()):
self.stdout.write("Aucun mail à envoyer.") self.stdout.write("Aucun mail à envoyer.")

View file

@ -2,14 +2,15 @@ import re
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.mail import send_mail from django.contrib.sites.models import Site
from django.core.mail import EmailMessage
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db import models, transaction from django.db import models, transaction
from django.db.models import F from django.db.models import F
from django.template import loader from django.template import loader
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
from shared.utils import choices_length from shared.utils import choices_length
@ -269,6 +270,32 @@ class Account(models.Model):
def __init__(self, trigramme): def __init__(self, trigramme):
self.trigramme = trigramme self.trigramme = trigramme
def send_creation_email(self):
"""
Envoie un mail à la création du trigramme.
"""
mail_data = settings.MAIL_DATA["kfet"]
email = EmailMessage(
subject="Création d'un trigramme",
body=loader.render_to_string(
"kfet/mails/creation_trigramme.txt",
context={
"account": self,
"site": Site.objects.get_current(),
"url_read": reverse("kfet.account.read", args=(self.trigramme)),
"url_update": reverse("kfet.account.update", args=(self.trigramme)),
"url_delete": reverse("kfet.account.delete", args=(self.trigramme))
},
),
from_email=mail_data["FROM"],
to=[self.email],
reply_to=[mail_data["REPLYTO"]],
)
# On envoie le mail
email.send()
def get_deleted_account(): def get_deleted_account():
return Account.objects.get(trigramme=KFET_DELETED_TRIGRAMME) return Account.objects.get(trigramme=KFET_DELETED_TRIGRAMME)
@ -298,10 +325,11 @@ class AccountNegative(models.Model):
""" """
Envoie un mail de rappel signalant que la personne est en négatif. Envoie un mail de rappel signalant que la personne est en négatif.
""" """
# On envoie le mail mail_data = settings.MAIL_DATA["kfet"]
send_mail(
"Compte K-Psul négatif", email = EmailMessage(
loader.render_to_string( subject="Compte K-Psul négatif",
body=loader.render_to_string(
"kfet/mails/rappel.txt", "kfet/mails/rappel.txt",
context={ context={
"account": self.account, "account": self.account,
@ -309,13 +337,17 @@ class AccountNegative(models.Model):
"start_date": self.start, "start_date": self.start,
}, },
), ),
settings.MAIL_DATA["rappel_negatif"]["FROM"], from_email=mail_data["FROM"],
[self.account.email], to=[self.account.email],
reply_to=[mail_data["REPLYTO"]],
) )
# On envoie le mail
email.send()
# On enregistre le fait que l'envoi a bien eu lieu # On enregistre le fait que l'envoi a bien eu lieu
self.last_rappel = timezone.now() self.last_rappel = timezone.now()
self.save() self.save()
return
class CheckoutQuerySet(models.QuerySet): class CheckoutQuerySet(models.QuerySet):

View file

@ -12,13 +12,15 @@ class OpenKfetConsumer(PermConsumerMixin, DjangoJsonWebsocketConsumer):
""" """
def connection_groups(self, user, **kwargs): async def open_status(self, event):
"""Select which group the user should be connected.""" await self.send_json(event)
if kfet_is_team(user):
return ["kfet.open.team"]
return ["kfet.open.base"]
def connect(self, message, *args, **kwargs): async def connect(self):
"""Send current status on connect.""" """Send current status on connect."""
super().connect(message, *args, **kwargs) await super().connect()
self.send(kfet_open.export(message.user))
group = "team" if kfet_is_team(self.user) else "base"
await self.channel_layer.group_add(f"kfet.open.{group}", self.channel_name)
await self.send_json(kfet_open.export(self.user))

View file

@ -1,5 +1,6 @@
from datetime import timedelta from datetime import timedelta
from channels.layers import get_channel_layer
from django.utils import timezone from django.utils import timezone
from ..decorators import kfet_is_team from ..decorators import kfet_is_team
@ -77,7 +78,7 @@ class OpenKfet(CachedMixin, object):
""" """
status = self.status() status = self.status()
base = {"status": status} base = {"status": status, "type": "open.status"}
restrict = { restrict = {
"admin_status": self.admin_status(status), "admin_status": self.admin_status(status),
"force_close": self.force_close, "force_close": self.force_close,
@ -95,13 +96,14 @@ class OpenKfet(CachedMixin, object):
base, team = self._export() base, team = self._export()
return team if kfet_is_team(user) else base return team if kfet_is_team(user) else base
def send_ws(self): async def send_ws(self):
"""Send internal state to websocket channels.""" """Send internal state to websocket channels."""
from .consumers import OpenKfetConsumer
base, team = self._export() base, team = self._export()
OpenKfetConsumer.group_send("kfet.open.base", base)
OpenKfetConsumer.group_send("kfet.open.team", team) channel_layer = get_channel_layer()
await channel_layer.group_send("kfet.open.base", base)
await channel_layer.group_send("kfet.open.team", team)
kfet_open = OpenKfet() kfet_open = OpenKfet()

View file

@ -1,5 +1,10 @@
from channels.routing import route_class from channels.routing import URLRouter
from django.urls import path
from . import consumers from .consumers import OpenKfetConsumer
routing = [route_class(consumers.OpenKfetConsumer)] OpenRouter = URLRouter(
[
path("", OpenKfetConsumer.as_asgi()),
]
)

View file

@ -1,19 +1,24 @@
import json
import random import random
from datetime import timedelta from datetime import timedelta
from unittest import mock from unittest import mock
from channels.channel import Group from asgiref.sync import async_to_sync
from channels.test import ChannelTestCase, WSClient from channels.auth import AuthMiddlewareStack
from channels.consumer import get_channel_layer
from channels.testing import WebsocketCommunicator
from django.contrib.auth.models import AnonymousUser, Permission, User from django.contrib.auth.models import AnonymousUser, Permission, User
from django.test import Client from django.test import Client, TestCase
from django.utils import timezone from django.utils import timezone
from . import OpenKfet from . import OpenKfet
from .consumers import OpenKfetConsumer from .consumers import OpenKfetConsumer
class OpenKfetTest(ChannelTestCase): def ws_communicator(cls, path: str, headers=[]):
return WebsocketCommunicator(AuthMiddlewareStack(cls.as_asgi()), path, headers)
class OpenKfetTest(TestCase):
"""OpenKfet object unit-tests suite.""" """OpenKfet object unit-tests suite."""
def setUp(self): def setUp(self):
@ -79,7 +84,7 @@ class OpenKfetTest(ChannelTestCase):
def test_export_user(self): def test_export_user(self):
"""Export is limited for an anonymous user.""" """Export is limited for an anonymous user."""
export = self.kfet_open.export(AnonymousUser()) export = self.kfet_open.export(AnonymousUser())
self.assertSetEqual(set(["status"]), set(export)) self.assertSetEqual(set(["status", "type"]), set(export))
def test_export_team(self): def test_export_team(self):
"""Export all values for a team member.""" """Export all values for a team member."""
@ -89,24 +94,32 @@ class OpenKfetTest(ChannelTestCase):
) )
user.user_permissions.add(is_team) user.user_permissions.add(is_team)
export = self.kfet_open.export(user) export = self.kfet_open.export(user)
self.assertSetEqual(set(["status", "admin_status", "force_close"]), set(export)) self.assertSetEqual(
set(["status", "admin_status", "force_close", "type"]), set(export)
)
def test_send_ws(self): async def test_send_ws(self):
Group("kfet.open.base").add("test.open.base") channel_layer = get_channel_layer()
Group("kfet.open.team").add("test.open.team") base_channel = await channel_layer.new_channel()
team_channel = await channel_layer.new_channel()
self.kfet_open.send_ws() await channel_layer.group_add("kfet.open.base", base_channel)
await channel_layer.group_add("kfet.open.team", team_channel)
recv_base = self.get_next_message("test.open.base", require=True) await self.kfet_open.send_ws()
base = json.loads(recv_base["text"])
self.assertSetEqual(set(["status"]), set(base))
recv_admin = self.get_next_message("test.open.team", require=True) base = await channel_layer.receive(base_channel)
admin = json.loads(recv_admin["text"])
self.assertSetEqual(set(["status", "admin_status", "force_close"]), set(admin)) self.assertSetEqual(set(["status", "type"]), set(base))
team = await channel_layer.receive(team_channel)
self.assertSetEqual(
set(["status", "admin_status", "force_close", "type"]), set(team)
)
class OpenKfetViewsTest(ChannelTestCase): class OpenKfetViewsTest(TestCase):
"""OpenKfet views unit-tests suite.""" """OpenKfet views unit-tests suite."""
def setUp(self): def setUp(self):
@ -177,119 +190,136 @@ class OpenKfetViewsTest(ChannelTestCase):
self.assertEqual(403, resp.status_code) self.assertEqual(403, resp.status_code)
class OpenKfetConsumerTest(ChannelTestCase): class OpenKfetConsumerTest(TestCase):
"""OpenKfet consumer unit-tests suite.""" """OpenKfet consumer unit-tests suite."""
def test_standard_user(self): @classmethod
"""Lambda user is added to kfet.open.base group.""" def setUpTestData(cls):
# setup anonymous client
c = WSClient()
# connect
c.send_and_consume(
"websocket.connect", path="/ws/k-fet/open", fail_on_none=True
)
# initialization data is replied on connection
self.assertIsNotNone(c.receive())
# client belongs to the 'kfet.open' group...
OpenKfetConsumer.group_send("kfet.open.base", {"test": "plop"})
self.assertEqual(c.receive(), {"test": "plop"})
# ...but not to the 'kfet.open.admin' one
OpenKfetConsumer.group_send("kfet.open.team", {"test": "plop"})
self.assertIsNone(c.receive())
@mock.patch("gestioncof.signals.messages")
def test_team_user(self, mock_messages):
"""Team user is added to kfet.open.team group."""
# setup team user and its client
t = User.objects.create_user("team", "", "team") t = User.objects.create_user("team", "", "team")
is_team = Permission.objects.get( is_team = Permission.objects.get(
codename="is_team", content_type__app_label="kfet" codename="is_team", content_type__app_label="kfet"
) )
t.user_permissions.add(is_team) t.user_permissions.add(is_team)
c = WSClient()
c.force_login(t, backend="django.contrib.auth.backends.ModelBackend")
# connect cls.team_user = t
c.send_and_consume(
"websocket.connect", path="/ws/k-fet/open", fail_on_none=True async def test_standard_user(self):
) """Lambda user is added to kfet.open.base group."""
# setup anonymous client
c = ws_communicator(OpenKfetConsumer, "/ws/k-fet/open")
connected, _ = await c.connect()
self.assertTrue(connected)
# initialization data is replied on connection # initialization data is replied on connection
self.assertIsNotNone(c.receive()) message = await c.receive_json_from()
self.assertIsNotNone(message)
# client belongs to the 'kfet.open.admin' group... # client belongs to the 'kfet.open' group...
OpenKfetConsumer.group_send("kfet.open.team", {"test": "plop"}) channel_layer = get_channel_layer()
self.assertEqual(c.receive(), {"test": "plop"})
# ... but not to the 'kfet.open' one await channel_layer.group_send(
OpenKfetConsumer.group_send("kfet.open.base", {"test": "plop"}) "kfet.open.base", {"test": "plop", "type": "open.status"}
self.assertIsNone(c.receive()) )
message = await c.receive_json_from()
self.assertEqual(message, {"test": "plop"})
# ...but not to the 'kfet.open.admin' one
await channel_layer.group_send(
"kfet.open.team", {"test": "plop", "type": "open.status"}
)
self.assertTrue(await c.receive_nothing())
async def test_team_user(self):
"""Team user is added to kfet.open.team group."""
# On simule l'appartenance de l'user à la team kfet car l'utilisation de
# tests async avec postgres fait tout planter si on modifie la db dans un
# des sous tests.
with mock.patch("gestioncof.signals.messages"), mock.patch(
"kfet.open.consumers.kfet_is_team", return_value=True
):
c = ws_communicator(OpenKfetConsumer, "/ws/k-fet/open")
connected, _ = await c.connect()
channel_layer = get_channel_layer()
self.assertTrue(connected)
# initialization data is replied on connection
message = await c.receive_json_from()
self.assertIsNotNone(message)
# client belongs to the 'kfet.open.team' group...
await channel_layer.group_send(
"kfet.open.team", {"test": "plop", "type": "open.status"}
)
message = await c.receive_json_from()
self.assertEqual(message, {"test": "plop"})
# ...but not to the 'kfet.open' one
await channel_layer.group_send(
"kfet.open.base", {"test": "plop", "type": "open.status"}
)
self.assertTrue(await c.receive_nothing())
class OpenKfetScenarioTest(ChannelTestCase): class OpenKfetScenarioTest(TestCase):
"""OpenKfet functionnal tests suite.""" """OpenKfet functionnal tests suite."""
def setUp(self): @classmethod
# Need this (and here) because of '<client>.login' in setUp def setUpTestData(cls):
patcher_messages = mock.patch("gestioncof.signals.messages") # root user
patcher_messages.start() cls.r = User.objects.create_superuser("team", "", "team")
self.addCleanup(patcher_messages.stop)
# anonymous client (for views) # anonymous client (for views)
self.c = Client() cls.c = Client()
# anonymous client (for websockets)
self.c_ws = WSClient()
# root user # root client
self.r = User.objects.create_superuser("root", "", "root") cls.r_c = Client()
# its client (for views)
self.r_c = Client() with mock.patch("gestioncof.signals.messages"):
self.r_c.login(username="root", password="root") cls.r_c.login(username="team", password="team")
# its client (for websockets)
self.r_c_ws = WSClient() def setUp(self):
self.r_c_ws.force_login( # Create channels to listen to messages
self.r, backend="django.contrib.auth.backends.ModelBackend" channel_layer = get_channel_layer()
)
self.channel = async_to_sync(channel_layer.new_channel)()
self.team_channel = async_to_sync(channel_layer.new_channel)()
async_to_sync(channel_layer.group_add)("kfet.open.base", self.channel)
async_to_sync(channel_layer.group_add)("kfet.open.team", self.team_channel)
self.receive_msg = lambda c: async_to_sync(channel_layer.receive)(c)
self.kfet_open = OpenKfet( self.kfet_open = OpenKfet(
cache_prefix="test_kfetopen_%s" % random.randrange(2**20) cache_prefix="test_kfetopen_%s" % random.randrange(2**20)
) )
self.addCleanup(self.kfet_open.clear_cache) self.addCleanup(self.kfet_open.clear_cache)
def ws_connect(self, ws_client): async def ws_connect(self, ws_communicator):
ws_client.send_and_consume( c, _ = await ws_communicator.connect()
"websocket.connect", path="/ws/k-fet/open", fail_on_none=True
)
return ws_client.receive(json=True)
def test_scenario_0(self): self.assertTrue(c)
"""Clients connect.""" return await ws_communicator.receive_json_from()
# test for anonymous user
msg = self.ws_connect(self.c_ws)
self.assertSetEqual(set(["status"]), set(msg))
# test for root user
msg = self.ws_connect(self.r_c_ws)
self.assertSetEqual(set(["status", "admin_status", "force_close"]), set(msg))
def test_scenario_1(self): def test_scenario_1(self):
"""Clients connect, door opens, enable force close.""" """Clients connect, door opens, enable force close."""
self.ws_connect(self.c_ws)
self.ws_connect(self.r_c_ws)
# door sent "I'm open!" # door sent "I'm open!"
self.c.post("/k-fet/open/raw_open", {"raw_open": True, "token": "plop"}) self.c.post("/k-fet/open/raw_open", {"raw_open": True, "token": "plop"})
# anonymous user agree # anonymous user agree
msg = self.c_ws.receive(json=True) msg = self.receive_msg(self.channel)
self.assertEqual(OpenKfet.OPENED, msg["status"]) self.assertEqual(OpenKfet.OPENED, msg["status"])
# root user too # root user too
msg = self.r_c_ws.receive(json=True) msg = self.receive_msg(self.team_channel)
self.assertEqual(OpenKfet.OPENED, msg["status"]) self.assertEqual(OpenKfet.OPENED, msg["status"])
self.assertEqual(OpenKfet.OPENED, msg["admin_status"]) self.assertEqual(OpenKfet.OPENED, msg["admin_status"])
@ -297,11 +327,11 @@ class OpenKfetScenarioTest(ChannelTestCase):
self.r_c.post("/k-fet/open/force_close", {"force_close": True}) self.r_c.post("/k-fet/open/force_close", {"force_close": True})
# so anonymous user see it's closed # so anonymous user see it's closed
msg = self.c_ws.receive(json=True) msg = self.receive_msg(self.channel)
self.assertEqual(OpenKfet.CLOSED, msg["status"]) self.assertEqual(OpenKfet.CLOSED, msg["status"])
# root user too # root user too
msg = self.r_c_ws.receive(json=True) msg = self.receive_msg(self.team_channel)
self.assertEqual(OpenKfet.CLOSED, msg["status"]) self.assertEqual(OpenKfet.CLOSED, msg["status"])
# but root knows things # but root knows things
self.assertEqual(OpenKfet.FAKE_CLOSED, msg["admin_status"]) self.assertEqual(OpenKfet.FAKE_CLOSED, msg["admin_status"])
@ -312,20 +342,42 @@ class OpenKfetScenarioTest(ChannelTestCase):
self.kfet_open.raw_open = True self.kfet_open.raw_open = True
self.kfet_open.force_close = True self.kfet_open.force_close = True
msg = self.ws_connect(self.c_ws) async_to_sync(OpenKfet().send_ws)()
msg = self.receive_msg(self.channel)
self.assertEqual(OpenKfet.CLOSED, msg["status"]) self.assertEqual(OpenKfet.CLOSED, msg["status"])
msg = self.ws_connect(self.r_c_ws) msg = self.receive_msg(self.team_channel)
self.assertEqual(OpenKfet.CLOSED, msg["status"]) self.assertEqual(OpenKfet.CLOSED, msg["status"])
self.assertEqual(OpenKfet.FAKE_CLOSED, msg["admin_status"]) self.assertEqual(OpenKfet.FAKE_CLOSED, msg["admin_status"])
self.assertTrue(msg["force_close"]) self.assertTrue(msg["force_close"])
self.r_c.post("/k-fet/open/force_close", {"force_close": False}) self.r_c.post("/k-fet/open/force_close", {"force_close": False})
msg = self.c_ws.receive(json=True) msg = self.receive_msg(self.channel)
self.assertEqual(OpenKfet.OPENED, msg["status"]) self.assertEqual(OpenKfet.OPENED, msg["status"])
msg = self.r_c_ws.receive(json=True) msg = self.receive_msg(self.team_channel)
self.assertEqual(OpenKfet.OPENED, msg["status"]) self.assertEqual(OpenKfet.OPENED, msg["status"])
self.assertEqual(OpenKfet.OPENED, msg["admin_status"]) self.assertEqual(OpenKfet.OPENED, msg["admin_status"])
self.assertFalse(msg["force_close"]) self.assertFalse(msg["force_close"])
async def test_scenario_3(self):
"""Clients connect."""
# anonymous client (for websockets)
self.c_ws = ws_communicator(OpenKfetConsumer, "/ws/k-fet/open")
# test for anonymous user
msg = await self.ws_connect(self.c_ws)
self.assertSetEqual(set(["status"]), set(msg))
# test for root user
with mock.patch(
"kfet.open.consumers.kfet_is_team", return_value=True
), mock.patch("kfet.open.open.kfet_is_team", return_value=True):
self.r_c_ws = ws_communicator(OpenKfetConsumer, "/ws/k-fet/open")
msg = await self.ws_connect(self.r_c_ws)
self.assertSetEqual(
set(["status", "admin_status", "force_close"]), set(msg)
)

View file

@ -1,3 +1,4 @@
from asgiref.sync import async_to_sync
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
@ -18,7 +19,7 @@ def raw_open(request):
raise PermissionDenied raise PermissionDenied
raw_open = request.POST.get("raw_open") in TRUE_STR raw_open = request.POST.get("raw_open") in TRUE_STR
kfet_open.raw_open = raw_open kfet_open.raw_open = raw_open
kfet_open.send_ws() async_to_sync(kfet_open.send_ws)()
return HttpResponse() return HttpResponse()
@ -27,5 +28,5 @@ def raw_open(request):
def force_close(request): def force_close(request):
force_close = request.POST.get("force_close") in TRUE_STR force_close = request.POST.get("force_close") in TRUE_STR
kfet_open.force_close = force_close kfet_open.force_close = force_close
kfet_open.send_ws() async_to_sync(kfet_open.send_ws)()
return HttpResponse() return HttpResponse()

View file

@ -1,8 +1,13 @@
from channels.routing import include, route_class from channels.routing import URLRouter
from django.urls import path
from . import consumers from kfet.open.routing import OpenRouter
routing = [ from .consumers import KPsul
route_class(consumers.KPsul, path=r"^/k-psul/$"),
include("kfet.open.routing.routing", path=r"^/open"), KFRouter = URLRouter(
] [
path("k-psul/", KPsul.as_asgi()),
path("open", OpenRouter),
]
)

View file

@ -78,7 +78,7 @@ class KfetWebsocket {
listen() { listen() {
var that = this; var that = this;
this.socket = new ReconnectingWebSocket(this.url); this.socket = new ReconnectingWebSocket(this.url, [], { minReconnectionDelay: 100 });
this.socket.onmessage = function (e) { this.socket.onmessage = function (e) {
var data = $.extend({}, that.default_msg, JSON.parse(e.data)); var data = $.extend({}, that.default_msg, JSON.parse(e.data));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
{% extends "kfet/base_form.html" %} {% extends "kfet/base_form.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Nouveau compte{% endblock %} {% block title %}Nouveau compte{% endblock %}
{% block header-title %}Création d'un compte{% endblock %} {% block header-title %}Création d'un compte{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "kfet/base.html" %} {% extends "kfet/base.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Nouveau compte{% endblock %} {% block title %}Nouveau compte{% endblock %}
{% block header-title %}Création d'un compte{% endblock %} {% block header-title %}Création d'un compte{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends 'kfet/base_form.html' %} {% extends 'kfet/base_form.html' %}
{% load staticfiles %} {% load static %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% block title %}Permissions - Édition{% endblock %} {% block title %}Permissions - Édition{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "kfet/base_col_2.html" %} {% extends "kfet/base_col_2.html" %}
{% load staticfiles %} {% load static %}
{% load kfet_tags %} {% load kfet_tags %}
{% load l10n %} {% load l10n %}
@ -8,7 +8,7 @@
<script type="text/javascript" src="{% url 'js_reverse' %}" ></script> <script type="text/javascript" src="{% url 'js_reverse' %}" ></script>
<script type="text/javascript" src="{% static 'kfet/vendor/moment/moment.min.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/vendor/moment/moment.min.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/vendor/moment/fr.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/vendor/moment/fr.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/vendor/moment/moment-timezone-with-data-2012-2022.min.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/vendor/moment/moment-timezone-with-data.min.js' %}"></script>
{% if account.user == request.user %} {% if account.user == request.user %}
<script type="text/javascript" src="{% static 'kfet/vendor/Chart.min.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/vendor/Chart.min.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/statistic.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/statistic.js' %}"></script>

View file

@ -1,5 +1,5 @@
{% extends 'kfet/base_col_2.html' %} {% extends 'kfet/base_col_2.html' %}
{% load staticfiles kfet_tags %} {% load static kfet_tags %}
{% block extra_head %} {% block extra_head %}
<script type="text/javascript" src="{% static 'kfet/vendor/Chart.min.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/vendor/Chart.min.js' %}"></script>

View file

@ -1,17 +1,13 @@
{% load wagtailcore_tags %} {% load wagtailcore_tags %}
{% with "k-fet@ens.fr" as kfet_mail %}
<footer class="row"> <footer class="row">
<div class="footer"> <div class="footer">
<span> <span>
<a href="{% slugurl "mentions-legales" %}">Mentions légales</a> <b><a href="{% url "kfet.contact" %}">Formulaire de contact</a></b>
</span> </span>
| |
<span> <span>
En cas de pépin : <a href="mailto:{{ kfet_mail }}"><tt>{{ kfet_mail }}</tt></a> <a href="{% slugurl "mentions-legales" %}">Mentions légales</a>
</span> </span>
</div> </div>
</div> </footer>
{% endwith %}

View file

@ -0,0 +1,36 @@
{% extends "kfet/base_form.html" %}
{% block extra_head %}
{{ negative_form.media }}
{% endblock %}
{% block title %}
Contacter la K-Fêt
{% endblock %}
{% block header-title %}
Contacter la K-Fêt
{% endblock %}
{% block footer %}
{% include "kfet/base_footer.html" %}
{% endblock %}
{% block main %}
<div class="messages">
<div class="alert alert-info">
<b>Votre message sera envoyé aux Chef·fe·s et aux Wo·men K-Fêt.</b>
</div>
</div>
<br>
<form action="" method="post" class="form-horizontal" autocomplete="off">
{% csrf_token %}
{% include 'kfet/form_snippet.html' %}
{% include 'kfet/form_submit_snippet.html' with value="Envoyer" %}
</form>
{% endblock %}

View file

@ -0,0 +1,28 @@
{% extends "kfet/base_form.html" %}
{% block extra_head %}
{{ negative_form.media }}
{% endblock %}
{% block title %}
Effectuer une demande de soirée
{% endblock %}
{% block header-title %}
Effectuer une demande de soirée
{% endblock %}
{% block footer %}
{% include "kfet/base_footer.html" %}
{% endblock %}
{% block main %}
<form action="" method="post" class="form-horizontal" autocomplete="off">
{% csrf_token %}
{% include 'kfet/form_snippet.html' %}
{% include 'kfet/form_submit_snippet.html' with value="Envoyer" %}
</form>
{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends 'kfet/base_col_2.html' %} {% extends 'kfet/base_col_2.html' %}
{% load l10n staticfiles widget_tweaks bootstrap %} {% load l10n static widget_tweaks bootstrap %}
{% block extra_head %} {% block extra_head %}
<link rel="stylesheet" type="text/css" href="{% static 'kfet/vendor/multiple-select/multiple-select.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'kfet/vendor/multiple-select/multiple-select.css' %}">
@ -7,7 +7,7 @@
{{ filter_form.media }} {{ filter_form.media }}
<script type="text/javascript" src="{% url 'js_reverse' %}" ></script> <script type="text/javascript" src="{% url 'js_reverse' %}" ></script>
<script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/vendor/moment/moment-timezone-with-data-2012-2022.min.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/vendor/moment/moment-timezone-with-data.min.js' %}"></script>
{% endblock %} {% endblock %}
{% block title %}Historique{% endblock %} {% block title %}Historique{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "kfet/base_col_1.html" %} {% extends "kfet/base_col_1.html" %}
{% load staticfiles %} {% load static %}
{% load kfet_tags %} {% load kfet_tags %}
{% block title %}Accueil{% endblock %} {% block title %}Accueil{% endblock %}

View file

@ -1,10 +1,10 @@
{% extends 'kfet/base.html' %} {% extends 'kfet/base.html' %}
{% load staticfiles %} {% load static %}
{% block extra_head %} {% block extra_head %}
<link rel="stylesheet" style="text/css" href="{% static 'kfet/css/kpsul_grid.css' %}"> <link rel="stylesheet" style="text/css" href="{% static 'kfet/css/kpsul_grid.css' %}">
<script type="text/javascript" src="{% static 'vendor/jquery/jquery.autocomplete-light.min.js' %}"></script> <script type="text/javascript" src="{% static 'vendor/jquery/jquery.autocomplete-light.min.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/vendor/moment/moment-timezone-with-data-2012-2022.min.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/vendor/moment/moment-timezone-with-data.min.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script>
<script type="text/javascript" src="{% url 'js_reverse' %}" ></script> <script type="text/javascript" src="{% url 'js_reverse' %}" ></script>
<script type="text/javascript" src="{% static 'kfet/vendor/underscore-min.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/vendor/underscore-min.js' %}"></script>

View file

@ -0,0 +1,12 @@
Salut {{ account.name }},
Ton compte K-Fêt a bien été créé le {{ account.created_at }} avec le trigramme {{ account.trigramme }}.
Tu peux désormais :
- Accéder à ton historique personnel des consommations : https://{{ site }}{{ url_read }}
- Modifier tes informations : https://{{ site }}{{ url_update }}
- Supprimer ton compte : https://{{ site }}{{ url_delete }}
En espérant te revoir bientôt,
--
L'équipe K-Fêt

View file

@ -0,0 +1,16 @@
Bonjour,
J'aimerais organiser une soirée le {{ date }}, au thème « {{ theme|safe }} », en K-Fêt.
Elle se terminerait à {{ horaire_fin }}, et le service serait en mode {{ service }}.
Les 4 responsables de la soirée seraient :
- {{ respo1 }}
- {{ respo2 }}
- {{ respo3 }}
- {{ respo4 }}
Quelques remarques supplémentaires :
{{ remarques|safe }}
Bien cordialement,
{{ nom|safe }}

View file

@ -1,6 +1,5 @@
{% extends 'kfet/base_col_2.html' %} {% extends 'kfet/base_col_2.html' %}
{% load staticfiles %} {% load l10n static widget_tweaks %}
{% load l10n staticfiles widget_tweaks %}
{% block title %}Transferts{% endblock %} {% block title %}Transferts{% endblock %}
{% block header-title %}Transferts{% endblock %} {% block header-title %}Transferts{% endblock %}
@ -8,7 +7,7 @@
{% block extra_head %} {% block extra_head %}
<script type="text/javascript" src="{% url 'js_reverse' %}" ></script> <script type="text/javascript" src="{% url 'js_reverse' %}" ></script>
<script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/vendor/moment/moment-timezone-with-data-2012-2022.min.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/vendor/moment/moment-timezone-with-data.min.js' %}"></script>
{% endblock %} {% endblock %}
{% block fixed %} {% block fixed %}

View file

@ -1,5 +1,5 @@
{% extends "kfet/base_col_1.html" %} {% extends "kfet/base_col_1.html" %}
{% load staticfiles %} {% load static %}
{% block extra_head %} {% block extra_head %}
<link rel="stylesheet" type="text/css" href="{% static 'kfet/css/transfers_form.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'kfet/css/transfers_form.css' %}">

View file

@ -94,6 +94,7 @@ class PermHelpersTest(TestCaseMixin, TestCase):
self.assertQuerysetEqual( self.assertQuerysetEqual(
user.user_permissions.all(), user.user_permissions.all(),
map(repr, [self.perm1, self.perm2, self.perm_team]), map(repr, [self.perm1, self.perm2, self.perm_team]),
transform=repr,
ordered=False, ordered=False,
) )

View file

@ -3,6 +3,8 @@ from datetime import datetime, timedelta
from decimal import Decimal from decimal import Decimal
from unittest import mock from unittest import mock
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import Client, TestCase from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
@ -518,6 +520,7 @@ class AccountGroupCreateViewTests(ViewTestCaseMixin, TestCase):
self.assertQuerysetEqual( self.assertQuerysetEqual(
group.permissions.all(), group.permissions.all(),
map(repr, [self.perms["kfet.is_team"], self.perms["kfet.manage_perms"]]), map(repr, [self.perms["kfet.is_team"], self.perms["kfet.manage_perms"]]),
transform=repr,
ordered=False, ordered=False,
) )
@ -571,6 +574,7 @@ class AccountGroupUpdateViewTests(ViewTestCaseMixin, TestCase):
self.assertQuerysetEqual( self.assertQuerysetEqual(
self.group.permissions.all(), self.group.permissions.all(),
map(repr, [self.perms["kfet.is_team"], self.perms["kfet.manage_perms"]]), map(repr, [self.perms["kfet.is_team"], self.perms["kfet.manage_perms"]]),
transform=repr,
ordered=False, ordered=False,
) )
@ -598,6 +602,7 @@ class AccountNegativeListViewTests(ViewTestCaseMixin, TestCase):
self.assertQuerysetEqual( self.assertQuerysetEqual(
r.context["negatives"], r.context["negatives"],
map(repr, [self.accounts["user"].negative]), map(repr, [self.accounts["user"].negative]),
transform=repr,
ordered=False, ordered=False,
) )
@ -698,7 +703,7 @@ class AccountStatOperationListViewTests(ViewTestCaseMixin, TestCase):
for stat, expected in zip(content["stats"], expected_stats): for stat, expected in zip(content["stats"], expected_stats):
expected_url = expected.pop("url") expected_url = expected.pop("url")
self.assertUrlsEqual(stat["url"], expected_url) self.assertUrlsEqual(stat["url"], expected_url)
self.assertDictContainsSubset(expected, stat) self.assertEqual(stat, {**stat, **expected})
class AccountStatOperationViewTests(ViewTestCaseMixin, TestCase): class AccountStatOperationViewTests(ViewTestCaseMixin, TestCase):
@ -807,7 +812,7 @@ class AccountStatBalanceListViewTests(ViewTestCaseMixin, TestCase):
for stat, expected in zip(content["stats"], expected_stats): for stat, expected in zip(content["stats"], expected_stats):
expected_url = expected.pop("url") expected_url = expected.pop("url")
self.assertUrlsEqual(stat["url"], expected_url) self.assertUrlsEqual(stat["url"], expected_url)
self.assertDictContainsSubset(expected, stat) self.assertEqual(stat, {**stat, **expected})
class AccountStatBalanceViewTests(ViewTestCaseMixin, TestCase): class AccountStatBalanceViewTests(ViewTestCaseMixin, TestCase):
@ -875,6 +880,7 @@ class CheckoutListViewTests(ViewTestCaseMixin, TestCase):
self.assertQuerysetEqual( self.assertQuerysetEqual(
r.context["checkouts"], r.context["checkouts"],
map(repr, [self.checkout1, self.checkout2]), map(repr, [self.checkout1, self.checkout2]),
transform=repr,
ordered=False, ordered=False,
) )
@ -1065,6 +1071,7 @@ class CheckoutStatementListViewTests(ViewTestCaseMixin, TestCase):
self.assertQuerysetEqual( self.assertQuerysetEqual(
r.context["checkoutstatements"], r.context["checkoutstatements"],
map(repr, expected_statements), map(repr, expected_statements),
transform=repr,
ordered=False, ordered=False,
) )
@ -1291,7 +1298,9 @@ class ArticleCategoryListViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertQuerysetEqual( self.assertQuerysetEqual(
r.context["categories"], map(repr, [self.category1, self.category2]) r.context["categories"],
map(repr, [self.category1, self.category2]),
transform=repr,
) )
@ -1366,7 +1375,9 @@ class ArticleListViewTests(ViewTestCaseMixin, TestCase):
r = self.client.get(self.url) r = self.client.get(self.url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertQuerysetEqual( self.assertQuerysetEqual(
r.context["articles"], map(repr, [self.article1, self.article2]) r.context["articles"],
map(repr, [self.article1, self.article2]),
transform=repr,
) )
@ -1636,7 +1647,7 @@ class ArticleStatSalesListViewTests(ViewTestCaseMixin, TestCase):
for stat, expected in zip(content["stats"], expected_stats): for stat, expected in zip(content["stats"], expected_stats):
expected_url = expected.pop("url") expected_url = expected.pop("url")
self.assertUrlsEqual(stat["url"], expected_url) self.assertUrlsEqual(stat["url"], expected_url)
self.assertDictContainsSubset(expected, stat) self.assertEqual(stat, {**stat, **expected})
class ArticleStatSalesViewTests(ViewTestCaseMixin, TestCase): class ArticleStatSalesViewTests(ViewTestCaseMixin, TestCase):
@ -1705,7 +1716,7 @@ class KPsulCheckoutDataViewTests(ViewTestCaseMixin, TestCase):
expected = {"name": "Checkout", "balance": "10.00"} expected = {"name": "Checkout", "balance": "10.00"}
self.assertDictContainsSubset(expected, content) self.assertEqual(content, {**content, **expected})
self.assertSetEqual( self.assertSetEqual(
set(content.keys()), set(content.keys()),
@ -1808,10 +1819,13 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.account.balance = Decimal("50.00") self.account.balance = Decimal("50.00")
self.account.save() self.account.save()
# Mock consumer of K-Psul websocket to catch what we're sending # Create a channel to listen to KPsul's messages
kpsul_consumer_patcher = mock.patch("kfet.consumers.KPsul") channel_layer = get_channel_layer()
self.kpsul_consumer_mock = kpsul_consumer_patcher.start() self.channel = async_to_sync(channel_layer.new_channel)()
self.addCleanup(kpsul_consumer_patcher.stop)
async_to_sync(channel_layer.group_add)("kfet.kpsul", self.channel)
self.receive_msg = lambda: async_to_sync(channel_layer.receive)(self.channel)
# Reset cache of kfet config # Reset cache of kfet config
kfet_config._conf_init = False kfet_config._conf_init = False
@ -2043,9 +2057,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(self.article.stock, 18) self.assertEqual(self.article.stock, 18)
# Check websocket data # Check websocket data
self.kpsul_consumer_mock.group_send.assert_called_once_with( ws_data = self.receive_msg()
"kfet.kpsul",
self.assertDictEqual(
ws_data,
{ {
"type": "kpsul",
"groups": [ "groups": [
{ {
"add": True, "add": True,
@ -2307,9 +2324,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.checkout.refresh_from_db() self.checkout.refresh_from_db()
self.assertEqual(self.checkout.balance, Decimal("110.75")) self.assertEqual(self.checkout.balance, Decimal("110.75"))
self.kpsul_consumer_mock.group_send.assert_called_once_with( ws_data = self.receive_msg()
"kfet.kpsul",
self.assertDictEqual(
ws_data,
{ {
"type": "kpsul",
"groups": [ "groups": [
{ {
"add": True, "add": True,
@ -2478,9 +2498,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.checkout.refresh_from_db() self.checkout.refresh_from_db()
self.assertEqual(self.checkout.balance, Decimal("89.25")) self.assertEqual(self.checkout.balance, Decimal("89.25"))
self.kpsul_consumer_mock.group_send.assert_called_once_with( ws_data = self.receive_msg()
"kfet.kpsul",
self.assertDictEqual(
ws_data,
{ {
"type": "kpsul",
"groups": [ "groups": [
{ {
"add": True, "add": True,
@ -2635,9 +2658,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.checkout.refresh_from_db() self.checkout.refresh_from_db()
self.assertEqual(self.checkout.balance, Decimal("100.00")) self.assertEqual(self.checkout.balance, Decimal("100.00"))
self.kpsul_consumer_mock.group_send.assert_called_once_with( ws_data = self.receive_msg()
"kfet.kpsul",
self.assertDictEqual(
ws_data,
{ {
"type": "kpsul",
"groups": [ "groups": [
{ {
"add": True, "add": True,
@ -2750,9 +2776,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.checkout.refresh_from_db() self.checkout.refresh_from_db()
self.assertEqual(self.checkout.balance, Decimal("100.00")) self.assertEqual(self.checkout.balance, Decimal("100.00"))
ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["groups"][0][ ws_data = self.receive_msg()
"entries" ws_data_ope = ws_data["groups"][0]["entries"][0]
][0]
self.assertEqual(ws_data_ope["addcost_amount"], Decimal("1.00")) self.assertEqual(ws_data_ope["addcost_amount"], Decimal("1.00"))
self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD") self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD")
@ -2790,9 +2816,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.checkout.refresh_from_db() self.checkout.refresh_from_db()
self.assertEqual(self.checkout.balance, Decimal("100.00")) self.assertEqual(self.checkout.balance, Decimal("100.00"))
ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["groups"][0][ ws_data = self.receive_msg()
"entries" ws_data_ope = ws_data["groups"][0]["entries"][0]
][0]
self.assertEqual(ws_data_ope["addcost_amount"], Decimal("0.80")) self.assertEqual(ws_data_ope["addcost_amount"], Decimal("0.80"))
self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD") self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD")
@ -2828,9 +2854,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.checkout.refresh_from_db() self.checkout.refresh_from_db()
self.assertEqual(self.checkout.balance, Decimal("106.00")) self.assertEqual(self.checkout.balance, Decimal("106.00"))
ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["groups"][0][ ws_data = self.receive_msg()
"entries" ws_data_ope = ws_data["groups"][0]["entries"][0]
][0]
self.assertEqual(ws_data_ope["addcost_amount"], Decimal("1.00")) self.assertEqual(ws_data_ope["addcost_amount"], Decimal("1.00"))
self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD") self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD")
@ -2864,9 +2890,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.accounts["addcost"].refresh_from_db() self.accounts["addcost"].refresh_from_db()
self.assertEqual(self.accounts["addcost"].balance, Decimal("15.00")) self.assertEqual(self.accounts["addcost"].balance, Decimal("15.00"))
ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["groups"][0][ ws_data = self.receive_msg()
"entries" ws_data_ope = ws_data["groups"][0]["entries"][0]
][0]
self.assertEqual(ws_data_ope["addcost_amount"], None) self.assertEqual(ws_data_ope["addcost_amount"], None)
self.assertEqual(ws_data_ope["addcost_for__trigramme"], None) self.assertEqual(ws_data_ope["addcost_for__trigramme"], None)
@ -2899,9 +2925,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.accounts["addcost"].refresh_from_db() self.accounts["addcost"].refresh_from_db()
self.assertEqual(self.accounts["addcost"].balance, Decimal("0.00")) self.assertEqual(self.accounts["addcost"].balance, Decimal("0.00"))
ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["groups"][0][ ws_data = self.receive_msg()
"entries" ws_data_ope = ws_data["groups"][0]["entries"][0]
][0]
self.assertEqual(ws_data_ope["addcost_amount"], None) self.assertEqual(ws_data_ope["addcost_amount"], None)
self.assertEqual(ws_data_ope["addcost_for__trigramme"], None) self.assertEqual(ws_data_ope["addcost_for__trigramme"], None)
@ -3123,9 +3149,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(article2.stock, -6) self.assertEqual(article2.stock, -6)
# Check websocket data # Check websocket data
self.kpsul_consumer_mock.group_send.assert_called_once_with( ws_data = self.receive_msg()
"kfet.kpsul",
self.assertDictEqual(
ws_data,
{ {
"type": "kpsul",
"groups": [ "groups": [
{ {
"add": True, "add": True,
@ -3218,10 +3247,14 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
self.account.balance = Decimal("50.00") self.account.balance = Decimal("50.00")
self.account.save() self.account.save()
# Mock consumer of K-Psul websocket to catch what we're sending # Create a channel to listen to KPsul's messages
kpsul_consumer_patcher = mock.patch("kfet.consumers.KPsul") channel_layer = get_channel_layer()
self.kpsul_consumer_mock = kpsul_consumer_patcher.start()
self.addCleanup(kpsul_consumer_patcher.stop) self.channel = async_to_sync(channel_layer.new_channel)()
async_to_sync(channel_layer.group_add)("kfet.kpsul", self.channel)
self.receive_msg = lambda: async_to_sync(channel_layer.receive)(self.channel)
def _assertResponseOk(self, response): def _assertResponseOk(self, response):
""" """
@ -3271,7 +3304,11 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
on_acc=self.account, on_acc=self.account,
checkout=self.checkout, checkout=self.checkout,
content=[ content=[
{"type": Operation.PURCHASE, "article": self.article, "article_nb": 2} {
"type": Operation.PURCHASE,
"article": self.article,
"article_nb": 2,
}
], ],
) )
operation = group.opes.get() operation = group.opes.get()
@ -3345,9 +3382,15 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
self.checkout.refresh_from_db() self.checkout.refresh_from_db()
self.assertEqual(self.checkout.balance, Decimal("100.00")) self.assertEqual(self.checkout.balance, Decimal("100.00"))
self.kpsul_consumer_mock.group_send.assert_called_with( ws_data = self.receive_msg()
"kfet.kpsul",
{"checkouts": [], "articles": [{"id": self.article.pk, "stock": 22}]}, self.assertDictEqual(
ws_data,
{
"type": "kpsul",
"checkouts": [],
"articles": [{"id": self.article.pk, "stock": 22}],
},
) )
def test_purchase_with_addcost(self): def test_purchase_with_addcost(self):
@ -3407,11 +3450,11 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
self.checkout.refresh_from_db() self.checkout.refresh_from_db()
self.assertEqual(self.checkout.balance, Decimal("95.00")) self.assertEqual(self.checkout.balance, Decimal("95.00"))
ws_data_checkouts = self.kpsul_consumer_mock.group_send.call_args[0][1][ ws_data = self.receive_msg()
"checkouts"
]
self.assertListEqual( self.assertListEqual(
ws_data_checkouts, [{"id": self.checkout.pk, "balance": Decimal("95.00")}] ws_data["checkouts"],
[{"id": self.checkout.pk, "balance": Decimal("95.00")}],
) )
def test_purchase_cash_with_addcost(self): def test_purchase_cash_with_addcost(self):
@ -3447,11 +3490,11 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
addcost_account.refresh_from_db() addcost_account.refresh_from_db()
self.assertEqual(addcost_account.balance, Decimal("9.00")) self.assertEqual(addcost_account.balance, Decimal("9.00"))
ws_data_checkouts = self.kpsul_consumer_mock.group_send.call_args[0][1][ ws_data = self.receive_msg()
"checkouts"
]
self.assertListEqual( self.assertListEqual(
ws_data_checkouts, [{"id": self.checkout.pk, "balance": Decimal("94.00")}] ws_data["checkouts"],
[{"id": self.checkout.pk, "balance": Decimal("94.00")}],
) )
@mock.patch("django.utils.timezone.now") @mock.patch("django.utils.timezone.now")
@ -3533,9 +3576,12 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
self.checkout.refresh_from_db() self.checkout.refresh_from_db()
self.assertEqual(self.checkout.balance, Decimal("89.25")) self.assertEqual(self.checkout.balance, Decimal("89.25"))
self.kpsul_consumer_mock.group_send.assert_called_with( ws_data = self.receive_msg()
"kfet.kpsul",
self.assertDictEqual(
ws_data,
{ {
"type": "kpsul",
"checkouts": [{"id": self.checkout.pk, "balance": Decimal("89.25")}], "checkouts": [{"id": self.checkout.pk, "balance": Decimal("89.25")}],
"articles": [], "articles": [],
}, },
@ -3620,9 +3666,12 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
self.checkout.refresh_from_db() self.checkout.refresh_from_db()
self.assertEqual(self.checkout.balance, Decimal("110.75")) self.assertEqual(self.checkout.balance, Decimal("110.75"))
self.kpsul_consumer_mock.group_send.assert_called_with( ws_data = self.receive_msg()
"kfet.kpsul",
self.assertDictEqual(
ws_data,
{ {
"type": "kpsul",
"checkouts": [{"id": self.checkout.pk, "balance": Decimal("110.75")}], "checkouts": [{"id": self.checkout.pk, "balance": Decimal("110.75")}],
"articles": [], "articles": [],
}, },
@ -3707,9 +3756,11 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
self.checkout.refresh_from_db() self.checkout.refresh_from_db()
self.assertEqual(self.checkout.balance, Decimal("100.00")) self.assertEqual(self.checkout.balance, Decimal("100.00"))
self.kpsul_consumer_mock.group_send.assert_called_with( ws_data = self.receive_msg()
"kfet.kpsul",
{"checkouts": [], "articles": []}, self.assertDictEqual(
ws_data,
{"type": "kpsul", "checkouts": [], "articles": []},
) )
@mock.patch("django.utils.timezone.now") @mock.patch("django.utils.timezone.now")
@ -4049,7 +4100,7 @@ class KPsulArticlesData(ViewTestCaseMixin, TestCase):
] ]
for expected, article in zip(expected_list, articles): for expected, article in zip(expected_list, articles):
self.assertDictContainsSubset(expected, article) self.assertEqual(article, {**article, **expected})
self.assertSetEqual( self.assertSetEqual(
set(article.keys()), set(article.keys()),
set( set(
@ -4149,7 +4200,7 @@ class AccountReadJSONViewTests(ViewTestCaseMixin, TestCase):
content = json.loads(r.content.decode("utf-8")) content = json.loads(r.content.decode("utf-8"))
expected = {"name": "first last", "trigramme": "000", "balance": "0.00"} expected = {"name": "first last", "trigramme": "000", "balance": "0.00"}
self.assertDictContainsSubset(expected, content) self.assertEqual(content, {**content, **expected})
self.assertSetEqual( self.assertSetEqual(
set(content.keys()), set(content.keys()),
@ -4393,7 +4444,9 @@ class InventoryListViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
inventories = r.context["inventories"] inventories = r.context["inventories"]
self.assertQuerysetEqual(inventories, map(repr, [self.inventory])) self.assertQuerysetEqual(
inventories, map(repr, [self.inventory]), transform=repr
)
class InventoryCreateViewTests(ViewTestCaseMixin, TestCase): class InventoryCreateViewTests(ViewTestCaseMixin, TestCase):
@ -4580,7 +4633,7 @@ class OrderListViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
orders = r.context["orders"] orders = r.context["orders"]
self.assertQuerysetEqual(orders, map(repr, [self.order])) self.assertQuerysetEqual(orders, map(repr, [self.order]), transform=repr)
class OrderReadViewTests(ViewTestCaseMixin, TestCase): class OrderReadViewTests(ViewTestCaseMixin, TestCase):
@ -4795,7 +4848,9 @@ class OrderToInventoryViewTests(ViewTestCaseMixin, TestCase):
inventory, inventory,
{"by": self.accounts["team1"], "at": self.now, "order": self.order}, {"by": self.accounts["team1"], "at": self.now, "order": self.order},
) )
self.assertQuerysetEqual(inventory.articles.all(), map(repr, [self.article])) self.assertQuerysetEqual(
inventory.articles.all(), map(repr, [self.article]), transform=repr
)
compte = InventoryArticle.objects.get(article=self.article) compte = InventoryArticle.objects.get(article=self.article)

View file

@ -9,6 +9,10 @@ register_converter(converters.TrigrammeConverter, "trigramme")
urlpatterns = [ urlpatterns = [
path("login/generic", views.login_generic, name="kfet.login.generic"), path("login/generic", views.login_generic, name="kfet.login.generic"),
path("history", views.history, name="kfet.history"), path("history", views.history, name="kfet.history"),
path("contact", views.ContactView.as_view(), name="kfet.contact"),
path(
"demande-soiree", views.DemandeSoireeView.as_view(), name="kfet.demande-soiree"
),
# ----- # -----
# Account urls # Account urls
# ----- # -----

View file

@ -1,8 +1,7 @@
import json import json
import math import math
from channels.channel import Group from channels.generic.websocket import AsyncJsonWebsocketConsumer
from channels.generic.websockets import JsonWebsocketConsumer
from django.core.cache import cache from django.core.cache import cache
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
@ -63,7 +62,7 @@ class CachedMixin:
# Consumers # Consumers
class DjangoJsonWebsocketConsumer(JsonWebsocketConsumer): class DjangoJsonWebsocketConsumer(AsyncJsonWebsocketConsumer):
"""Custom Json Websocket Consumer. """Custom Json Websocket Consumer.
Encode to JSON with DjangoJSONEncoder. Encode to JSON with DjangoJSONEncoder.
@ -71,7 +70,10 @@ class DjangoJsonWebsocketConsumer(JsonWebsocketConsumer):
""" """
@classmethod @classmethod
def encode_json(cls, content): async def encode_json(cls, content):
# Remove the type value, only used by Channels to choose the group to send to
content.pop("type")
return json.dumps(content, cls=DjangoJSONEncoder) return json.dumps(content, cls=DjangoJSONEncoder)
@ -89,31 +91,11 @@ class PermConsumerMixin:
http_user = True # Enable message.user http_user = True # Enable message.user
perms_connect = [] perms_connect = []
def connect(self, message, **kwargs): async def connect(self):
"""Check permissions on connection.""" """Check permissions on connection."""
if message.user.has_perms(self.perms_connect): self.user = self.scope["user"]
super().connect(message, **kwargs)
if self.user.has_perms(self.perms_connect):
await super().connect()
else: else:
self.close() await self.close()
def raw_connect(self, message, **kwargs):
# Same as original raw_connect method of JsonWebsocketConsumer
# We add user to connection_groups call.
groups = self.connection_groups(user=message.user, **kwargs)
for group in groups:
Group(group, channel_layer=message.channel_layer).add(message.reply_channel)
self.connect(message, **kwargs)
def raw_disconnect(self, message, **kwargs):
# Same as original raw_connect method of JsonWebsocketConsumer
# We add user to connection_groups call.
groups = self.connection_groups(user=message.user, **kwargs)
for group in groups:
Group(group, channel_layer=message.channel_layer).discard(
message.reply_channel
)
self.disconnect(message, **kwargs)
def connection_groups(self, user, **kwargs):
"""`message.user` is available as `user` arg. Original behavior."""
return super().connection_groups(user=user, **kwargs)

View file

@ -13,6 +13,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.auth.models import Permission, User from django.contrib.auth.models import Permission, User
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import SuspiciousOperation from django.core.exceptions import SuspiciousOperation
from django.core.mail import EmailMessage
from django.db import transaction from django.db import transaction
from django.db.models import ( from django.db.models import (
Count, Count,
@ -34,6 +35,7 @@ from django.http import (
JsonResponse, JsonResponse,
) )
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.template import loader
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@ -42,10 +44,11 @@ from django.views.generic.detail import BaseDetailView
from django.views.generic.edit import CreateView, DeleteView, UpdateView from django.views.generic.edit import CreateView, DeleteView, UpdateView
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
from kfet import KFET_DELETED_TRIGRAMME, consumers from kfet import KFET_DELETED_TRIGRAMME
from kfet.auth.decorators import kfet_password_auth from kfet.auth.decorators import kfet_password_auth
from kfet.autocomplete import kfet_account_only_autocomplete, kfet_autocomplete from kfet.autocomplete import kfet_account_only_autocomplete, kfet_autocomplete
from kfet.config import kfet_config from kfet.config import kfet_config
from kfet.consumers import KPsul
from kfet.decorators import teamkfet_required from kfet.decorators import teamkfet_required
from kfet.forms import ( from kfet.forms import (
AccountForm, AccountForm,
@ -63,6 +66,8 @@ from kfet.forms import (
CheckoutStatementCreateForm, CheckoutStatementCreateForm,
CheckoutStatementUpdateForm, CheckoutStatementUpdateForm,
CofForm, CofForm,
ContactForm,
DemandeSoireeForm,
FilterHistoryForm, FilterHistoryForm,
InventoryArticleForm, InventoryArticleForm,
KFetConfigForm, KFetConfigForm,
@ -113,6 +118,61 @@ def put_cleaned_data_in_dict(dict, form):
dict[field] = form.cleaned_data[field] dict[field] = form.cleaned_data[field]
class ContactView(FormView):
template_name = "kfet/contact.html"
form_class = ContactForm
success_url = reverse_lazy("kfet.contact")
def form_valid(self, form):
# Envoie un mail lorsque le formulaire est valide
EmailMessage(
form.cleaned_data["subject"],
form.cleaned_data["message"],
from_email=form.cleaned_data["from_email"],
to=("chefs-k-fet@ens.psl.eu",),
).send()
messages.success(
self.request,
"Votre message a bien été envoyé aux Wo·men K-Fêt.",
)
return super().form_valid(form)
class DemandeSoireeView(FormView):
template_name = "kfet/demande_soiree.html"
form_class = DemandeSoireeForm
success_url = reverse_lazy("kfet.demande-soiree")
def form_valid(self, form):
destinataires = ["chefs-k-fet@ens.psl.eu"]
if form.cleaned_data["contact_boum"]:
destinataires.append("boum@ens.psl.eu")
if form.cleaned_data["contact_pls"]:
destinataires.append("pls@ens.psl.eu")
# Envoie un mail lorsque le formulaire est valide
EmailMessage(
f"Demande de soirée le {form.cleaned_data['date']}",
loader.render_to_string(
"kfet/mails/demande_soiree.txt", context=form.cleaned_data
),
from_email=form.cleaned_data["from_email"],
to=destinataires,
cc=[form.cleaned_data["from_email"]],
).send()
messages.success(
self.request,
"Votre demande de soirée a bien été envoyée.",
)
return super().form_valid(form)
# ----- # -----
# Account views # Account views
# ----- # -----
@ -184,6 +244,7 @@ def account_create(request):
account_form = AccountNoTriForm(request.POST, instance=account) account_form = AccountNoTriForm(request.POST, instance=account)
account_form.save() account_form.save()
messages.success(request, "Compte créé : %s" % account.trigramme) messages.success(request, "Compte créé : %s" % account.trigramme)
account.send_creation_email()
return redirect("kfet.account.create") return redirect("kfet.account.create")
except Account.UserHasAccount as e: except Account.UserHasAccount as e:
messages.error( messages.error(
@ -361,7 +422,7 @@ def account_update(request, trigramme):
user_info_form = UserInfoForm(instance=account.user) user_info_form = UserInfoForm(instance=account.user)
account_form = AccountForm(instance=account) account_form = AccountForm(instance=account)
group_form = UserGroupForm(instance=account.user) group_form = UserGroupForm(instance=account.user)
frozen_form = AccountFrozenForm(request.POST, instance=account) frozen_form = AccountFrozenForm(instance=account)
pwd_form = AccountPwdForm() pwd_form = AccountPwdForm()
if request.method == "POST": if request.method == "POST":
@ -994,8 +1055,13 @@ def kpsul_update_addcost(request):
kfet_config.set(addcost_for=account, addcost_amount=amount) kfet_config.set(addcost_for=account, addcost_amount=amount)
data = {"addcost": {"for": account and account.trigramme or None, "amount": amount}} data = {
consumers.KPsul.group_send("kfet.kpsul", data) "addcost": {"for": account and account.trigramme or None, "amount": amount},
"type": "kpsul",
}
KPsul.group_send("kfet.kpsul", data)
return JsonResponse(data) return JsonResponse(data)
@ -1179,7 +1245,7 @@ def kpsul_perform_operations(request):
) )
# Websocket data # Websocket data
websocket_data = {} websocket_data = {"type": "kpsul"}
websocket_data["groups"] = [ websocket_data["groups"] = [
{ {
"add": True, "add": True,
@ -1226,7 +1292,9 @@ def kpsul_perform_operations(request):
websocket_data["articles"].append( websocket_data["articles"].append(
{"id": article["id"], "stock": article["stock"]} {"id": article["id"], "stock": article["stock"]}
) )
consumers.KPsul.group_send("kfet.kpsul", websocket_data)
KPsul.group_send("kfet.kpsul", websocket_data)
return JsonResponse(data) return JsonResponse(data)
@ -1421,7 +1489,7 @@ def cancel_operations(request):
articles = Article.objects.values("id", "stock").filter(pk__in=articles_pk) articles = Article.objects.values("id", "stock").filter(pk__in=articles_pk)
# Websocket data # Websocket data
websocket_data = {"checkouts": [], "articles": []} websocket_data = {"checkouts": [], "articles": [], "type": "kpsul"}
for checkout in checkouts: for checkout in checkouts:
websocket_data["checkouts"].append( websocket_data["checkouts"].append(
{"id": checkout["id"], "balance": checkout["balance"]} {"id": checkout["id"], "balance": checkout["balance"]}
@ -1430,7 +1498,8 @@ def cancel_operations(request):
websocket_data["articles"].append( websocket_data["articles"].append(
{"id": article["id"], "stock": article["stock"]} {"id": article["id"], "stock": article["stock"]}
) )
consumers.KPsul.group_send("kfet.kpsul", websocket_data)
KPsul.group_send("kfet.kpsul", websocket_data)
data["canceled"] = list(opes) data["canceled"] = list(opes)
data["opegroups_to_update"] = list(opegroups) data["opegroups_to_update"] = list(opegroups)

View file

@ -3,7 +3,7 @@ import os
import sys import sys
if __name__ == "__main__": if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestioasso.settings.local") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestioasso.settings")
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line

View file

@ -2,7 +2,7 @@ from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.db.models import Min from django.db.models import Min
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from shared.utils import choices_length from shared.utils import choices_length
@ -44,9 +44,13 @@ class PetitCoursAbility(models.Model):
class Meta: class Meta:
app_label = "gestioncof" app_label = "gestioncof"
constraints = [
models.UniqueConstraint(
fields=["user", "niveau", "matiere"], name="unique_competence_level"
)
]
verbose_name = "Compétence petits cours" verbose_name = "Compétence petits cours"
verbose_name_plural = "Compétences des petits cours" verbose_name_plural = "Compétences des petits cours"
unique_together = ("user", "niveau", "matiere")
def __str__(self): def __str__(self):
return "{:s} - {!s} - {:s}".format( return "{:s} - {!s} - {:s}".format(

View file

@ -1,5 +1,5 @@
{% extends "petitscours/base_title.html" %} {% extends "petitscours/base_title.html" %}
{% load staticfiles %} {% load static %}
{% block page_size %}col-sm-8{% endblock %} {% block page_size %}col-sm-8{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "petitscours/base_title.html" %} {% extends "petitscours/base_title.html" %}
{% load staticfiles %} {% load static %}
{% block realcontent %} {% block realcontent %}
<h2>Demandes de petits cours</h2> <h2>Demandes de petits cours</h2>

View file

@ -1,4 +1,4 @@
{% load staticfiles %} {% load static %}
<table class="table table-striped"> <table class="table table-striped">
<tr class="danger"><td><strong>Date</strong></td><td> {{ demande.created }}</td></tr> <tr class="danger"><td><strong>Date</strong></td><td> {{ demande.created }}</td></tr>
<tr class="warning"><td><strong>Nom/prénom</strong></td><td> {{ demande.name }}</td></tr> <tr class="warning"><td><strong>Nom/prénom</strong></td><td> {{ demande.name }}</td></tr>

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load staticfiles %} {% load static %}
{% block extra_head %} {% block extra_head %}
<script src="{% static 'vendor/jquery/jquery-ui.min.js' %}" type="text/javascript"></script> <script src="{% static 'vendor/jquery/jquery-ui.min.js' %}" type="text/javascript"></script>

View file

@ -10,14 +10,14 @@ Le COF a reçu une demande de petit cours qui te correspond. Tu es en haut de la
¤ Lieu (si préféré) : {{ demande.lieu }} ¤ Lieu (si préféré) : {{ demande.lieu }}
¤ Niveau : {{ demande.get_niveau_display }}
¤ Remarques diverses (désolé pour les balises HTML) : {{ demande.remarques }}
{% if matieres|length > 1 %}¤ Matières : {% if matieres|length > 1 %}¤ Matières :
{% for matiere in matieres %} ¤ {{ matiere }} {% for matiere in matieres %} ¤ {{ matiere }}
{% endfor %}{% else %}¤ Matière : {% for matiere in matieres %}{{ matiere }} {% endfor %}{% else %}¤ Matière : {% for matiere in matieres %}{{ matiere }}
{% endfor %}{% endif %} {% endfor %}{% endif %}
¤ Niveau : {{ demande.get_niveau_display }}
¤ Remarques diverses (désolé pour les balises HTML) : {{ demande.remarques }}
Voilà, cette personne te contactera peut-être sous peu, tu pourras voir les détails directement avec elle (prix, modalités, ...). Pour indication, 30 Euro/h semble être la moyenne. Voilà, cette personne te contactera peut-être sous peu, tu pourras voir les détails directement avec elle (prix, modalités, ...). Pour indication, 30 Euro/h semble être la moyenne.
Si tu te rends compte qu'en fait tu ne peux pas/plus donner de cours en ce moment, ça serait cool que tu décoches la case "Recevoir des propositions de petits cours" sur GestioCOF. Ensuite dès que tu voudras réapparaître tu pourras recocher la case et tu seras à nouveau sur la liste. Si tu te rends compte qu'en fait tu ne peux pas/plus donner de cours en ce moment, ça serait cool que tu décoches la case "Recevoir des propositions de petits cours" sur GestioCOF. Ensuite dès que tu voudras réapparaître tu pourras recocher la case et tu seras à nouveau sur la liste.

View file

@ -1,5 +1,5 @@
{% extends "petitscours/base_title.html" %} {% extends "petitscours/base_title.html" %}
{% load staticfiles %} {% load static %}
{% block realcontent %} {% block realcontent %}
<h2> <h2>

View file

@ -15,8 +15,8 @@ server {
rewrite ^/gestion$ http://localhost:8080/gestion/ redirect; rewrite ^/gestion$ http://localhost:8080/gestion/ redirect;
# Les pages statiques sont servies à part. # Les pages statiques sont servies à part.
location /gestion/static { try_files $uri $uri/ =404; } location /static { try_files $uri $uri/ =404; }
location /gestion/media { try_files $uri $uri/ =404; } location /media { try_files $uri $uri/ =404; }
# On proxy-pass les requêtes vers les pages dynamiques à daphne # On proxy-pass les requêtes vers les pages dynamiques à daphne
location / { location / {

View file

@ -11,7 +11,7 @@ WorkingDirectory=/vagrant
Environment="DJANGO_SETTINGS_MODULE=gestioasso.settings.dev" Environment="DJANGO_SETTINGS_MODULE=gestioasso.settings.dev"
ExecStart=/home/vagrant/venv/bin/daphne \ ExecStart=/home/vagrant/venv/bin/daphne \
-u /srv/gestiocof/gestiocof.sock \ -u /srv/gestiocof/gestiocof.sock \
cof.asgi:channel_layer gestioasso.asgi:application
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View file

@ -10,7 +10,10 @@ Group=vagrant
TimeoutSec=300 TimeoutSec=300
WorkingDirectory=/vagrant WorkingDirectory=/vagrant
Environment="DJANGO_SETTINGS_MODULE=gestioasso.settings.dev" Environment="DJANGO_SETTINGS_MODULE=gestioasso.settings.dev"
ExecStart=/home/vagrant/venv/bin/python manage.py runworker ExecStart=/home/vagrant/venv/bin/python manage.py runworker \
'kfet.open.team' \
'kfet.open.base' \
'kpsul'
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View file

@ -3,6 +3,6 @@ django-debug-toolbar==3.2.*
ipython ipython
# Tools # Tools
black black==22.3.0
flake8 flake8
isort isort

View file

@ -1,16 +1,15 @@
-r requirements.txt -r requirements.txt
# Postgresql bindings # Postgresql bindings
psycopg2<2.8 psycopg2==2.9.*
# Redis # Redis
django-redis-cache==2.1.* django-redis-cache==3.0.*
redis~=2.10.6 redis==3.5.*
asgi-redis==1.4.* channels-redis==3.4.*
# ASGI protocol and HTTP server # ASGI protocol and HTTP server
asgiref~=1.1.2 daphne==3.0.*
daphne==1.3.0
# ldap bindings # ldap bindings
python-ldap python-ldap

View file

@ -1,19 +1,19 @@
Django==2.2.* Django==3.2.*
Pillow==7.2.0 Pillow==7.2.0
authens==0.1b0 authens==0.1b4
channels==1.1.* channels==3.0.*
configparser==3.5.0 configparser==3.5.0
django-autocomplete-light==3.3.* django-autocomplete-light==3.9.4
django-bootstrap-form==3.3 django-bootstrap-form==3.3
django-cas-ng==3.6.* django-cas-ng==4.3.*
django-cors-headers==2.2.0 django-cors-headers==3.13.0
django-djconfig==0.8.0 django-djconfig==0.10.0
django-hCaptcha==0.1.0 django-hCaptcha==0.2.0
django-js-reverse==0.9.1 django-js-reverse==0.9.1
django-widget-tweaks==1.4.1 django-widget-tweaks==1.4.1
icalendar==4.0.7 icalendar==4.0.7
python-dateutil==2.8.1 python-dateutil==2.8.1
statistics==1.0.3.5 statistics==1.0.3.5
wagtail-modeltranslation==0.10.* wagtail-modeltranslation==0.11.*
wagtail==2.7.* wagtail==2.13.*
wagtailmenus==3.0.* wagtailmenus==3.0.*

View file

@ -3,8 +3,8 @@ source =
bda bda
bds bds
clubs clubs
cof
events events
gestioasso
gestioncof gestioncof
kfet kfet
petitscours petitscours

Some files were not shown because too many files have changed in this diff Show more