1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-07-20 16:09:12 +02:00

Compare commits

..

8 Commits

Author SHA1 Message Date
c28dfdc15d Merge branch 'qrcode' into 'main'
Draft: Qrcode

See merge request bde/nk20!196
2025-06-14 23:24:55 +02:00
e6f3084588 Added a first pass for automatically entering an activity with a qrcode 2023-10-11 18:01:51 +02:00
145e55da75 remove useless comment 2022-03-22 15:06:04 +01:00
d3ba95cdca Insecable space for more clarity 2022-03-22 15:04:41 +01:00
8ffb0ebb56 Use DetailView 2022-03-22 14:59:01 +01:00
5038af9e34 Final html template 2022-03-22 14:58:26 +01:00
819b4214c9 Add QRCode View, URL and test template 2022-03-22 12:26:44 +01:00
b8a93b0b75 Add link to QR code 2022-03-19 16:25:15 +01:00
64 changed files with 1928 additions and 4821 deletions

View File

@ -21,6 +21,3 @@ EMAIL_PASSWORD=CHANGE_ME
# Wiki configuration # Wiki configuration
WIKI_USER=NoteKfet2020 WIKI_USER=NoteKfet2020
WIKI_PASSWORD= WIKI_PASSWORD=
# OIDC
OIDC_RSA_PRIVATE_KEY=CHANGE_ME

View File

@ -8,7 +8,7 @@ variables:
GIT_SUBMODULE_STRATEGY: recursive GIT_SUBMODULE_STRATEGY: recursive
# Ubuntu 22.04 # Ubuntu 22.04
py310-django52: py310-django42:
stage: test stage: test
image: ubuntu:22.04 image: ubuntu:22.04
before_script: before_script:
@ -22,10 +22,10 @@ py310-django52:
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py310-django52 script: tox -e py310-django42
# Debian Bookworm # Debian Bookworm
py311-django52: py311-django42:
stage: test stage: test
image: debian:bookworm image: debian:bookworm
before_script: before_script:
@ -37,7 +37,7 @@ py311-django52:
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py311-django52 script: tox -e py311-django42
linters: linters:
stage: quality-assurance stage: quality-assurance

View File

@ -61,8 +61,8 @@ Bien que cela permette de créer une instance sur toutes les distributions,
6. (Optionnel) **Création d'une clé privée OpenID Connect** 6. (Optionnel) **Création d'une clé privée OpenID Connect**
Pour activer le support d'OpenID Connect, il faut générer une clé privée, par Pour activer le support d'OpenID Connect, il faut générer une clé privée, par
exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et copier la clé dans .env dans le champ exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et renseigner son
`OIDC_RSA_PRIVATE_KEY`. emplacement dans `OIDC_RSA_PRIVATE_KEY` (par défaut `/var/secrets/oidc.key`).
7. Enjoy : 7. Enjoy :
@ -237,8 +237,8 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous.
7. **Création d'une clé privée OpenID Connect** 7. **Création d'une clé privée OpenID Connect**
Pour activer le support d'OpenID Connect, il faut générer une clé privée, par Pour activer le support d'OpenID Connect, il faut générer une clé privée, par
exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et renseigner le champ exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et renseigner son
`OIDC_RSA_PRIVATE_KEY` dans le .env (par défaut `/var/secrets/oidc.key`). emplacement dans `OIDC_RSA_PRIVATE_KEY` (par défaut `/var/secrets/oidc.key`).
8. *Enjoy \o/* 8. *Enjoy \o/*

View File

@ -38,6 +38,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
</a> </a>
<input id="alias" type="text" class="form-control" placeholder="Nom/note ..."> <input id="alias" type="text" class="form-control" placeholder="Nom/note ...">
<button id="trigger" class="btn btn-secondary">Click me !</button>
<hr> <hr>
@ -63,15 +64,46 @@ SPDX-License-Identifier: GPL-3.0-or-later
refreshBalance(); refreshBalance();
} }
function process_qrcode() {
let name = alias_obj.val();
$.get("/api/note/note?search=" + name + "&format=json").done(
function (res) {
let note = res.results[0];
$.post("/api/activity/entry/?format=json", {
csrfmiddlewaretoken: CSRF_TOKEN,
activity: {{ activity.id }},
note: note.id,
guest: null
}).done(function () {
addMsg(interpolate(gettext(
"Entry made for %s whose balance is %s €"),
[note.name, note.balance / 100]), "success", 4000);
reloadTable(true);
}).fail(function (xhr) {
errMsg(xhr.responseJSON, 4000);
});
}).fail(function (xhr) {
errMsg(xhr.responseJSON, 4000);
});
}
alias_obj.keyup(function(event) { alias_obj.keyup(function(event) {
let code = event.originalEvent.keyCode let code = event.originalEvent.keyCode
if (65 <= code <= 122 || code === 13) { if (65 <= code <= 122 || code === 13) {
debounce(reloadTable)() debounce(reloadTable)()
} }
if (code === 0)
process_qrcode();
}); });
$(document).ready(init); $(document).ready(init);
alias_obj2 = document.getElementById("alias");
$("#trigger").click(function (e) {
addMsg("Clicked", "success", 1000);
alias_obj.val(alias_obj.val() + "\0");
alias_obj2.dispatchEvent(new KeyboardEvent('keyup'));
})
function init() { function init() {
$(".table-row").click(function (e) { $(".table-row").click(function (e) {
let target = e.target.parentElement; let target = e.target.parentElement;

View File

@ -145,7 +145,7 @@ class AddIngredientForms(forms.ModelForm):
polymorphic_ctype__model="transformedfood", polymorphic_ctype__model="transformedfood",
is_ready=False, is_ready=False,
end_of_life='', end_of_life='',
).filter(PermissionBackend.filter_queryset(get_current_request(), Food, "change")).exclude(pk=pk) ).filter(PermissionBackend.filter_queryset(get_current_request(), TransformedFood, "change")).exclude(pk=pk)
class Meta: class Meta:
model = TransformedFood model = TransformedFood

View File

@ -12,9 +12,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
</h3> </h3>
<div class="card-body"> <div class="card-body">
<ul> <ul>
{% if QR_code %}
<li> {{QR_code}} </li>
{% endif %}
{% for field, value in fields %} {% for field, value in fields %}
<li> {{ field }} : {{ value }}</li> <li> {{ field }} : {{ value }}</li>
{% endfor %} {% endfor %}

View File

@ -7,52 +7,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% load i18n %} {% load i18n %}
{% block content %} {% block content %}
<div class="card bg-light"> {{ block.super }}
<h3 class="card-header text-center">
{{ title }}
</h3>
<div class="card-body">
<style>
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
appearance: textfield;
padding: 6px;
border: 1px solid #ccc;
border-radius: 4px;
width: 100px;
}
</style>
<div class="d-flex align-items-center" style="max-width: 300px;">
<form method="get" action="{% url 'food:redirect_view' %}" class="d-flex w-100">
<input type="number" name="slug" placeholder="QR-code" required class="form-control form-control-sm" style="max-width: 120px;">
<button type="submit" class="btn btn-sm btn-primary">{% trans "View food" %}</button>
</form>
</div>
</div>
<div class="card-body">
<input id="searchbar" type="text" class="form-control"
placeholder="{% trans "Search by attribute such as name..." %}">
</div>
{% block extra_inside_card %}
{% endblock %}
<div id="dynamic-table">
{% if table.data %}
{% render_table table %}
{% else %}
<div class="card-body">
<div class="alert alert-warning">
{% trans "There is no results." %}
</div>
</div>
{% endif %}
</div>
</div>
<br> <br>
<div class="card bg-light mb-3"> <div class="card bg-light mb-3">
<h3 class="card-header text-center"> <h3 class="card-header text-center">
@ -113,20 +68,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('goButton').addEventListener('click', function(event) {
event.preventDefault();
const slug = document.getElementById('slugInput').value;
if (slug && !isNaN(slug)) {
window.location.href = `/food/${slug}/`;
} else {
alert("Veuillez entrer un nombre valide.");
}
});
});
</script>
{% endblock %} {% endblock %}

View File

@ -18,5 +18,4 @@ urlpatterns = [
path('detail/basic/<int:pk>', views.BasicFoodDetailView.as_view(), name='basicfood_view'), path('detail/basic/<int:pk>', views.BasicFoodDetailView.as_view(), name='basicfood_view'),
path('detail/transformed/<int:pk>', views.TransformedFoodDetailView.as_view(), name='transformedfood_view'), path('detail/transformed/<int:pk>', views.TransformedFoodDetailView.as_view(), name='transformedfood_view'),
path('add/ingredient/<int:pk>', views.AddIngredientView.as_view(), name='add_ingredient'), path('add/ingredient/<int:pk>', views.AddIngredientView.as_view(), name='add_ingredient'),
path('redirect/', views.QRCodeRedirectView.as_view(), name='redirect_view'),
] ]

View File

@ -10,7 +10,6 @@ from django.db.models import Q
from django.http import HttpResponseRedirect, Http404 from django.http import HttpResponseRedirect, Http404
from django.views.generic import DetailView, UpdateView, CreateView from django.views.generic import DetailView, UpdateView, CreateView
from django.views.generic.list import ListView from django.views.generic.list import ListView
from django.views.generic.base import RedirectView
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 gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -455,8 +454,6 @@ class FoodDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context["fields"] = [( context["fields"] = [(
Food._meta.get_field(field).verbose_name.capitalize(), Food._meta.get_field(field).verbose_name.capitalize(),
value) for field, value in fields.items()] value) for field, value in fields.items()]
if self.object.QR_code.exists():
context["QR_code"] = self.object.QR_code.first()
context["meals"] = self.object.transformed_ingredient_inv.all() context["meals"] = self.object.transformed_ingredient_inv.all()
context["update"] = PermissionBackend.check_perm(self.request, "food.change_food") context["update"] = PermissionBackend.check_perm(self.request, "food.change_food")
context["add_ingredient"] = (self.object.end_of_life == '' and PermissionBackend.check_perm(self.request, "food.change_transformedfood")) context["add_ingredient"] = (self.object.end_of_life == '' and PermissionBackend.check_perm(self.request, "food.change_transformedfood"))
@ -510,14 +507,3 @@ class TransformedFoodDetailView(FoodDetailView):
if Food.objects.filter(pk=kwargs['pk']).count() == 1: if Food.objects.filter(pk=kwargs['pk']).count() == 1:
kwargs['stop_redirect'] = (Food.objects.get(pk=kwargs['pk']).polymorphic_ctype.model == 'transformedfood') kwargs['stop_redirect'] = (Food.objects.get(pk=kwargs['pk']).polymorphic_ctype.model == 'transformedfood')
return super().get(*args, **kwargs) return super().get(*args, **kwargs)
class QRCodeRedirectView(RedirectView):
"""
Redirects to the QR code creation page from Food List
"""
def get_redirect_url(self, *args, **kwargs):
slug = self.request.GET.get('slug')
if slug:
return reverse_lazy('food:qrcode_create', kwargs={'slug': slug})
return reverse_lazy('food:list')

View File

@ -6,7 +6,7 @@ from django.conf import settings
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .signals import save_user_profile, update_wei_registration_fee_on_membership_creation, update_wei_registration_fee_on_club_change from .signals import save_user_profile
class MemberConfig(AppConfig): class MemberConfig(AppConfig):
@ -17,16 +17,7 @@ class MemberConfig(AppConfig):
""" """
Define app internal signals to interact with other apps Define app internal signals to interact with other apps
""" """
from .models import Membership, Club
post_save.connect( post_save.connect(
save_user_profile, save_user_profile,
sender=settings.AUTH_USER_MODEL, sender=settings.AUTH_USER_MODEL,
) )
post_save.connect(
update_wei_registration_fee_on_membership_creation,
sender=Membership
)
post_save.connect(
update_wei_registration_fee_on_club_change,
sender=Club
)

View File

@ -438,6 +438,8 @@ class Membership(models.Model):
) )
if hasattr(self, '_force_renew_parent') and self._force_renew_parent: if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
new_membership._force_renew_parent = True new_membership._force_renew_parent = True
if hasattr(self, '_soge') and self._soge:
new_membership._soge = True
if hasattr(self, '_force_save') and self._force_save: if hasattr(self, '_force_save') and self._force_save:
new_membership._force_save = True new_membership._force_save = True
new_membership.save() new_membership.save()
@ -456,6 +458,8 @@ class Membership(models.Model):
# Renew the previous membership of the parent club # Renew the previous membership of the parent club
parent_membership = parent_membership.first() parent_membership = parent_membership.first()
parent_membership._force_renew_parent = True parent_membership._force_renew_parent = True
if hasattr(self, '_soge'):
parent_membership._soge = True
if hasattr(self, '_force_save'): if hasattr(self, '_force_save'):
parent_membership._force_save = True parent_membership._force_save = True
parent_membership.renew() parent_membership.renew()
@ -467,6 +471,8 @@ class Membership(models.Model):
date_start=self.date_start, date_start=self.date_start,
) )
parent_membership._force_renew_parent = True parent_membership._force_renew_parent = True
if hasattr(self, '_soge'):
parent_membership._soge = True
if hasattr(self, '_force_save'): if hasattr(self, '_force_save'):
parent_membership._force_save = True parent_membership._force_save = True
parent_membership.save() parent_membership.save()

View File

@ -13,25 +13,3 @@ def save_user_profile(instance, created, raw, **_kwargs):
instance.profile.email_confirmed = True instance.profile.email_confirmed = True
instance.profile.registration_valid = True instance.profile.registration_valid = True
instance.profile.save() instance.profile.save()
def update_wei_registration_fee_on_membership_creation(sender, instance, created, **kwargs):
if created:
from wei.models import WEIRegistration
if instance.club.id == 1 or instance.club.id == 2:
registrations = WEIRegistration.objects.filter(
user=instance.user,
wei__year=instance.date_start.year,
)
for r in registrations:
r.save()
def update_wei_registration_fee_on_club_change(sender, instance, **kwargs):
from wei.models import WEIRegistration
if instance.id == 1 or instance.id == 2:
registrations = WEIRegistration.objects.filter(
wei__year=instance.membership_start.year,
)
for r in registrations:
r.save()

View File

@ -60,7 +60,10 @@
{% if user_object.pk == user.pk %} {% if user_object.pk == user.pk %}
<div class="text-center"> <div class="text-center">
<a class="small badge badge-secondary" href="{% url 'member:auth_token' %}"> <a class="small badge badge-secondary" href="{% url 'member:auth_token' %}">
<i class="fa fa-cogs"></i>{% trans 'API token' %} <i class="fa fa-cogs"></i>&nbsp;{% trans 'API token' %}
</a>
<a class="small badge badge-secondary" href="{% url 'member:qr_code' user_object.pk %}">
<i class="fa fa-qrcode"></i>&nbsp;{% trans 'QR Code' %}
</a> </a>
</div> </div>
{% endif %} {% endif %}

View File

@ -0,0 +1,36 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% block content %}
<div class="card bg-light">
<h3 class="card-header text-center">
{% trans "QR Code for" %} {{ user_object.username }} ({{ user_object.first_name }} {{user_object.last_name }})
</h3>
<div class="text-center" id="qrcode">
</div>
</div>
{% endblock %}
{% block extrajavascript %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAUSHSQX0FslNhTDadL4O5SAGapGt4FodqL8My0mA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
var qrc = new QRCode(document.getElementById("qrcode"), {
text: "{{ user_object.pk }}\0",
width: 1024,
height: 1024
});
</script>
{% endblock %}
{% block extracss %}
<style>
img {
width: 100%
}
</style>
{% endblock %}

View File

@ -44,7 +44,7 @@ class TemplateLoggedInTests(TestCase):
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL, 302, 302) self.assertRedirects(response, settings.LOGIN_REDIRECT_URL, 302, 302)
def test_logout(self): def test_logout(self):
response = self.client.post(reverse("logout")) response = self.client.get(reverse("logout"))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_admin_index(self): def test_admin_index(self):

View File

@ -25,4 +25,5 @@ urlpatterns = [
path('user/<int:pk>/aliases/', views.ProfileAliasView.as_view(), name="user_alias"), path('user/<int:pk>/aliases/', views.ProfileAliasView.as_view(), name="user_alias"),
path('user/<int:pk>/trust', views.ProfileTrustView.as_view(), name="user_trust"), path('user/<int:pk>/trust', views.ProfileTrustView.as_view(), name="user_trust"),
path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'), path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
path('user/<int:pk>/qr_code/', views.QRCodeView.as_view(), name='qr_code'),
] ]

View File

@ -402,6 +402,14 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
context['token'] = Token.objects.get_or_create(user=self.request.user)[0] context['token'] = Token.objects.get_or_create(user=self.request.user)[0]
return context return context
class QRCodeView(LoginRequiredMixin, DetailView):
"""
Affiche le QR Code
"""
model = User
context_object_name = "user_object"
template_name = "member/qr_code.html"
extra_context = {"title": _("QR Code")}
# ******************************* # # ******************************* #
# CLUB # # CLUB #

View File

@ -13,7 +13,7 @@ def register_note_urls(router, path):
router.register(path + '/note', NotePolymorphicViewSet) router.register(path + '/note', NotePolymorphicViewSet)
router.register(path + '/alias', AliasViewSet) router.register(path + '/alias', AliasViewSet)
router.register(path + '/trust', TrustViewSet) router.register(path + '/trust', TrustViewSet)
router.register(path + '/consumer', ConsumerViewSet, basename='alias2') router.register(path + '/consumer', ConsumerViewSet)
router.register(path + '/transaction/category', TemplateCategoryViewSet) router.register(path + '/transaction/category', TemplateCategoryViewSet)
router.register(path + '/transaction/transaction', TransactionViewSet) router.register(path + '/transaction/transaction', TransactionViewSet)

View File

@ -1695,7 +1695,7 @@
"wei", "wei",
"weimembership" "weimembership"
], ],
"query": "{\"club\": [\"club\"]}", "query": "[\"AND\", {\"club\": [\"club\"], \"club__weiclub__membership_end__gte\": [\"today\"]}, [\"OR\", {\"registration__soge_credit\": true}, {\"user__note__balance__gte\": {\"F\": [\"F\", \"fee\"]}}]]",
"type": "add", "type": "add",
"mask": 2, "mask": 2,
"field": "", "field": "",
@ -3998,358 +3998,6 @@
"description": "Créer une transaction de ou vers la note d'un club tant que la source reste au dessus de -50 €" "description": "Créer une transaction de ou vers la note d'un club tant que la source reste au dessus de -50 €"
} }
}, },
{
"model": "permission.permission",
"pk": 271,
"fields": {
"model": [
"wei",
"bus"
],
"query": "{\"wei\": [\"club\"]}",
"type": "change",
"mask": 3,
"field": "",
"permanent": false,
"description": "Modifier n'importe quel bus du wei"
}
},
{
"model": "permission.permission",
"pk": 272,
"fields": {
"model": [
"wei",
"bus"
],
"query": "{\"wei\": [\"club\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir tous les bus du wei"
}
},
{
"model": "permission.permission",
"pk": 273,
"fields": {
"model": [
"wei",
"busteam"
],
"query": "{\"bus__wei\": [\"club\"], \"bus__wei__membership_end__gte\": [\"today\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir toutes les équipes WEI"
}
},
{
"model": "permission.permission",
"pk": 274,
"fields": {
"model": [
"member",
"club"
],
"query": "{\"bus__wei\": [\"club\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir les informations de clubs des bus"
}
},
{
"model": "permission.permission",
"pk": 275,
"fields": {
"model": [
"member",
"club"
],
"query": "{\"bus__wei\": [\"club\"]}",
"type": "change",
"mask": 3,
"field": "",
"permanent": false,
"description": "Modifier les clubs des bus"
}
},
{
"model": "permission.permission",
"pk": 276,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club__bus__wei\": [\"club\"]}",
"type": "add",
"mask": 3,
"field": "",
"permanent": false,
"description": "Ajouter un⋅e membre à un club de bus"
}
},
{
"model": "permission.permission",
"pk": 277,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club__bus__wei\": [\"club\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir les adhérents d'un club de bus"
}
},
{
"model": "permission.permission",
"pk": 278,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club__bus__wei\": [\"club\"]}",
"type": "change",
"mask": 3,
"field": "",
"permanent": false,
"description": "Modifier l'adhésion d'un club de bus"
}
},
{
"model": "permission.permission",
"pk": 279,
"fields": {
"model": [
"note",
"note"
],
"query": "{\"noteclub__club__bus__wei\": [\"club\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir la note d'un club de bus"
}
},
{
"model": "permission.permission",
"pk": 280,
"fields": {
"model": [
"note",
"transaction"
],
"query": "[\"OR\", {\"source__noteclub__club__bus__wei\": [\"club\"]}, {\"destination__noteclub__club__bus__wei\": [\"club\"]}]",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir les transactions d'un club de bus"
}
},
{
"model": "permission.permission",
"pk": 281,
"fields": {
"model": [
"note",
"transaction"
],
"query": "[\"AND\", [\"OR\", {\"source__noteclub__club__bus__wei\": [\"club\"]}, {\"destination__noteclub__club__bus__wei\": [\"club\"]}], [\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 2000]}}, {\"valid\": false}]]",
"type": "add",
"mask": 3,
"field": "",
"permanent": false,
"description": "Créer une transaction d'un club de bus tant que la source reste au dessus de -20 €"
}
},
{
"model": "permission.permission",
"pk": 282,
"fields": {
"model": [
"note",
"transaction"
],
"query": "[\"AND\", [\"OR\", {\"source__noteclub__club\": [\"club\"]}, {\"destination__noteclub__club\": [\"club\"]}], [\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 2000]}}, {\"valid\": false}]]",
"type": "add",
"mask": 3,
"field": "",
"permanent": false,
"description": "Créer une transaction d'un WEI tant que la source reste au dessus de -20 €"
}
},
{
"model": "permission.permission",
"pk": 283,
"fields": {
"model": [
"auth",
"user"
],
"query": "{\"memberships__club__name\": \"Kfet\", \"memberships__roles__name\": \"Adh\u00e9rent\u22c5e Kfet\", \"memberships__date_start__lte\": [\"today\"], \"memberships__date_end__gte\": [\"today\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir n'importe quel⋅le utilisateur⋅rice qui est adhérent⋅e Kfet"
}
},
{
"model": "permission.permission",
"pk": 284,
"fields": {
"model": [
"member",
"club"
],
"query": "{\"bus\": [\"membership\", \"weimembership\", \"bus\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir les informations de club de son bus"
}
},
{
"model": "permission.permission",
"pk": 285,
"fields": {
"model": [
"member",
"club"
],
"query": "{\"bus\": [\"membership\", \"weimembership\", \"bus\"]}",
"type": "change",
"mask": 3,
"field": "",
"permanent": false,
"description": "Modifier le club de son bus"
}
},
{
"model": "permission.permission",
"pk": 286,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club__bus\": [\"membership\", \"weimembership\", \"bus\"]}",
"type": "add",
"mask": 3,
"field": "",
"permanent": false,
"description": "Ajouter un⋅e membre au club de son bus"
}
},
{
"model": "permission.permission",
"pk": 287,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club__bus\": [\"membership\", \"weimembership\", \"bus\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir les adhérents du club de son bus"
}
},
{
"model": "permission.permission",
"pk": 288,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club__bus\": [\"membership\", \"weimembership\", \"bus\"]}",
"type": "change",
"mask": 3,
"field": "",
"permanent": false,
"description": "Modifier l'adhésion au club de son bus"
}
},
{
"model": "permission.permission",
"pk": 289,
"fields": {
"model": [
"note",
"note"
],
"query": "{\"noteclub__club__bus\": [\"membership\", \"weimembership\", \"bus\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir la note du club de son bus"
}
},
{
"model": "permission.permission",
"pk": 290,
"fields": {
"model": [
"note",
"transaction"
],
"query": "[\"OR\", {\"source__noteclub__club__bus\": [\"membership\", \"weimembership\", \"bus\"]}, {\"destination__noteclub__club__bus\": [\"membership\", \"weimembership\", \"bus\"]}]",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir les transactions du club de son bus"
}
},
{
"model": "permission.permission",
"pk": 291,
"fields": {
"model": [
"wei",
"bus"
],
"query": "{\"pk\": [\"membership\", \"weimembership\", \"bus\", \"pk\"], \"wei__date_end__gte\": [\"today\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir mon bus"
}
},
{
"model": "permission.permission",
"pk": 292,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club__pk__lte\": 2}",
"type": "add",
"mask": 3,
"field": "",
"permanent": false,
"description": "Ajouter un membre au BDE ou à la Kfet"
}
},
{ {
"model": "permission.role", "model": "permission.role",
"pk": 1, "pk": 1,
@ -4710,8 +4358,6 @@
"name": "GC WEI", "name": "GC WEI",
"permissions": [ "permissions": [
22, 22,
49,
62,
70, 70,
72, 72,
76, 76,
@ -4736,23 +4382,7 @@
112, 112,
113, 113,
128, 128,
130, 130
142,
269,
271,
272,
273,
274,
275,
276,
277,
278,
279,
280,
281,
282,
283,
292
] ]
} }
}, },
@ -4771,14 +4401,7 @@
119, 119,
120, 120,
121, 121,
122, 122
284,
285,
286,
287,
289,
290,
291
] ]
} }
}, },
@ -4810,6 +4433,18 @@
] ]
} }
}, },
{
"model": "permission.role",
"pk": 16,
"fields": {
"for_club": null,
"name": "\u00c9lectron libre (avec perm)",
"permissions": [
22,
84
]
}
},
{ {
"model": "permission.role", "model": "permission.role",
"pk": 17, "pk": 17,
@ -4964,14 +4599,7 @@
119, 119,
120, 120,
121, 121,
122, 122
284,
285,
286,
287,
289,
290,
291
] ]
} }
}, },
@ -5081,6 +4709,11 @@
"pk": 15, "pk": 15,
"fields": {} "fields": {}
}, },
{
"model": "wei.weirole",
"pk": 16,
"fields": {}
},
{ {
"model": "wei.weirole", "model": "wei.weirole",
"pk": 17, "pk": 17,

View File

@ -1,6 +1,5 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from oauth2_provider.oauth2_validators import OAuth2Validator from oauth2_provider.oauth2_validators import OAuth2Validator
from oauth2_provider.scopes import BaseScopes from oauth2_provider.scopes import BaseScopes
from member.models import Club from member.models import Club
@ -18,39 +17,22 @@ class PermissionScopes(BaseScopes):
and can be useful to make queries through the API with limited privileges. and can be useful to make queries through the API with limited privileges.
""" """
def get_all_scopes(self, **kwargs): def get_all_scopes(self):
scopes = {} return {f"{p.id}_{club.id}": f"{p.description} (club {club.name})"
if 'scopes' in kwargs:
for scope in kwargs['scopes']:
if scope == 'openid':
scopes['openid'] = "OpenID Connect"
else:
p = Permission.objects.get(id=scope.split('_')[0])
club = Club.objects.get(id=scope.split('_')[1])
scopes[scope] = f"{p.description} (club {club.name})"
return scopes
scopes = {f"{p.id}_{club.id}": f"{p.description} (club {club.name})"
for p in Permission.objects.all() for club in Club.objects.all()} for p in Permission.objects.all() for club in Club.objects.all()}
scopes['openid'] = "OpenID Connect"
return scopes
def get_available_scopes(self, application=None, request=None, *args, **kwargs): def get_available_scopes(self, application=None, request=None, *args, **kwargs):
if not application: if not application:
return [] return []
scopes = [f"{p.id}_{p.membership.club.id}" return [f"{p.id}_{p.membership.club.id}"
for t in Permission.PERMISSION_TYPES for t in Permission.PERMISSION_TYPES
for p in PermissionBackend.get_raw_permissions(get_current_request(), t[0])] for p in PermissionBackend.get_raw_permissions(get_current_request(), t[0])]
scopes.append('openid')
return scopes
def get_default_scopes(self, application=None, request=None, *args, **kwargs): def get_default_scopes(self, application=None, request=None, *args, **kwargs):
if not application: if not application:
return [] return []
scopes = [f"{p.id}_{p.membership.club.id}" return [f"{p.id}_{p.membership.club.id}"
for p in PermissionBackend.get_raw_permissions(get_current_request(), 'view')] for p in PermissionBackend.get_raw_permissions(get_current_request(), 'view')]
scopes.append('openid')
return scopes
class PermissionOAuth2Validator(OAuth2Validator): class PermissionOAuth2Validator(OAuth2Validator):
@ -67,10 +49,6 @@ class PermissionOAuth2Validator(OAuth2Validator):
"email": request.user.email, "email": request.user.email,
} }
def get_discovery_claims(self, request):
claims = super().get_discovery_claims(self)
return claims + ["name", "normalized_name", "email"]
def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs): def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
""" """
User can request as many scope as he wants, including invalid scopes, User can request as many scope as he wants, including invalid scopes,
@ -88,8 +66,6 @@ class PermissionOAuth2Validator(OAuth2Validator):
if scope in scopes: if scope in scopes:
valid_scopes.add(scope) valid_scopes.add(scope)
if 'openid' in scopes:
valid_scopes.add('openid')
request.scopes = valid_scopes request.scopes = valid_scopes
return valid_scopes return valid_scopes

View File

@ -13,14 +13,12 @@ EXCLUDED = [
'cas_server.serviceticket', 'cas_server.serviceticket',
'cas_server.user', 'cas_server.user',
'cas_server.userattributes', 'cas_server.userattributes',
'constance.constance',
'contenttypes.contenttype', 'contenttypes.contenttype',
'logs.changelog', 'logs.changelog',
'migrations.migration', 'migrations.migration',
'oauth2_provider.accesstoken', 'oauth2_provider.accesstoken',
'oauth2_provider.grant', 'oauth2_provider.grant',
'oauth2_provider.refreshtoken', 'oauth2_provider.refreshtoken',
'oauth2_provider.idtoken',
'sessions.session', 'sessions.session',
] ]

View File

@ -10,7 +10,7 @@ from django.utils import timezone
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from activity.models import Activity from activity.models import Activity
from member.models import Club, Membership from member.models import Club, Membership
from note.models import NoteUser, NoteClub from note.models import NoteUser
from wei.models import WEIClub, Bus, WEIRegistration from wei.models import WEIClub, Bus, WEIRegistration
@ -122,13 +122,10 @@ class TestPermissionDenied(TestCase):
def test_validate_weiregistration(self): def test_validate_weiregistration(self):
wei = WEIClub.objects.create( wei = WEIClub.objects.create(
name="WEI Test",
membership_start=date.today(), membership_start=date.today(),
date_start=date.today() + timedelta(days=1), date_start=date.today() + timedelta(days=1),
date_end=date.today() + timedelta(days=1), date_end=date.today() + timedelta(days=1),
parent_club=Club.objects.get(name="Kfet"),
) )
NoteClub.objects.create(club=wei)
registration = WEIRegistration.objects.create(wei=wei, user=self.user, birth_date="2000-01-01") registration = WEIRegistration.objects.create(wei=wei, user=self.user, birth_date="2000-01-01")
response = self.client.get(reverse("wei:validate_registration", kwargs=dict(pk=registration.pk))) response = self.client.get(reverse("wei:validate_registration", kwargs=dict(pk=registration.pk)))
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)

View File

@ -164,24 +164,14 @@ class ScopesView(LoginRequiredMixin, TemplateView):
from oauth2_provider.models import Application from oauth2_provider.models import Application
from .scopes import PermissionScopes from .scopes import PermissionScopes
oidc = False scopes = PermissionScopes()
context["scopes"] = {} context["scopes"] = {}
all_scopes = scopes.get_all_scopes()
for app in Application.objects.filter(user=self.request.user).all(): for app in Application.objects.filter(user=self.request.user).all():
available_scopes = PermissionScopes().get_available_scopes(app) available_scopes = scopes.get_available_scopes(app)
context["scopes"][app] = OrderedDict() context["scopes"][app] = OrderedDict()
all_scopes = PermissionScopes().get_all_scopes(scopes=available_scopes) items = [(k, v) for (k, v) in all_scopes.items() if k in available_scopes]
scopes = {}
for scope in available_scopes:
scopes[scope] = all_scopes[scope]
# remove OIDC scope for sort
if 'openid' in scopes:
del scopes['openid']
oidc = True
items = [(k, v) for (k, v) in scopes.items()]
items.sort(key=lambda x: (int(x[0].split("_")[1]), int(x[0].split("_")[0]))) items.sort(key=lambda x: (int(x[0].split("_")[1]), int(x[0].split("_")[0])))
# add oidc if necessary
if oidc:
items.append(('openid', PermissionScopes().get_all_scopes(scopes=['openid'])['openid']))
for k, v in items: for k, v in items:
context["scopes"][app][k] = v context["scopes"][app][k] = v

View File

@ -353,7 +353,7 @@ class SogeCredit(models.Model):
def amount(self): def amount(self):
if self.valid: if self.valid:
return self.credit_transaction.total return self.credit_transaction.total
amount = sum(max(transaction.total - 2000, 0) for transaction in self.transactions.all()) amount = sum(transaction.total for transaction in self.transactions.all())
if 'wei' in settings.INSTALLED_APPS: if 'wei' in settings.INSTALLED_APPS:
from wei.models import WEIMembership from wei.models import WEIMembership
if not WEIMembership.objects\ if not WEIMembership.objects\
@ -441,7 +441,7 @@ class SogeCredit(models.Model):
With Great Power Comes Great Responsibility... With Great Power Comes Great Responsibility...
""" """
total_fee = sum(max(transaction.total - 2000, 0) for transaction in self.transactions.all() if not transaction.valid) total_fee = sum(transaction.total for transaction in self.transactions.all() if not transaction.valid)
if self.user.note.balance < total_fee: if self.user.note.balance < total_fee:
raise ValidationError(_("This user doesn't have enough money to pay the memberships with its note. " raise ValidationError(_("This user doesn't have enough money to pay the memberships with its note. "
"Please ask her/him to credit the note before invalidating this credit.")) "Please ask her/him to credit the note before invalidating this credit."))

View File

@ -77,7 +77,7 @@ class WEIRegistrationViewSet(ReadProtectedModelViewSet):
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['user', 'user__username', 'user__first_name', 'user__last_name', 'user__email', filterset_fields = ['user', 'user__username', 'user__first_name', 'user__last_name', 'user__email',
'user__note__alias__name', 'user__note__alias__normalized_name', 'wei', 'wei__name', 'user__note__alias__name', 'user__note__alias__normalized_name', 'wei', 'wei__name',
'wei__email', 'wei__year', 'soge_credit', 'deposit_check', 'birth_date', 'gender', 'wei__email', 'wei__year', 'soge_credit', 'caution_check', 'birth_date', 'gender',
'clothing_cut', 'clothing_size', 'first_year', 'emergency_contact_name', 'clothing_cut', 'clothing_size', 'first_year', 'emergency_contact_name',
'emergency_contact_phone', ] 'emergency_contact_phone', ]
search_fields = ['$user__username', '$user__first_name', '$user__last_name', '$user__email', search_fields = ['$user__username', '$user__first_name', '$user__last_name', '$user__email',

View File

@ -1,8 +1,7 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from .registration import WEIForm, WEIRegistrationForm, WEIMembership1AForm, \ from .registration import WEIForm, WEIRegistrationForm, WEIMembership1AForm, WEIMembershipForm, BusForm, BusTeamForm
WEIMembershipForm, BusForm, BusTeamForm
from .surveys import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, CurrentSurvey from .surveys import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, CurrentSurvey
__all__ = [ __all__ = [

View File

@ -5,7 +5,7 @@ from bootstrap_datepicker_plus.widgets import DatePickerInput
from django import forms from django import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Q from django.db.models import Q
from django.forms import CheckboxSelectMultiple, RadioSelect from django.forms import CheckboxSelectMultiple
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial, NoteUser from note.models import NoteSpecial, NoteUser
from note_kfet.inputs import AmountInput, Autocomplete, ColorWidget from note_kfet.inputs import AmountInput, Autocomplete, ColorWidget
@ -24,8 +24,6 @@ class WEIForm(forms.ModelForm):
"membership_end": DatePickerInput(), "membership_end": DatePickerInput(),
"date_start": DatePickerInput(), "date_start": DatePickerInput(),
"date_end": DatePickerInput(), "date_end": DatePickerInput(),
"deposit_amount": AmountInput(),
"fee_soge_credit": AmountInput(),
} }
@ -41,11 +39,7 @@ class WEIRegistrationForm(forms.ModelForm):
class Meta: class Meta:
model = WEIRegistration model = WEIRegistration
fields = [ exclude = ('wei', 'clothing_cut')
'user', 'soge_credit', 'birth_date', 'gender', 'clothing_size',
'health_issues', 'emergency_contact_name', 'emergency_contact_phone',
'first_year', 'information_json', 'deposit_check', 'deposit_type'
]
widgets = { widgets = {
"user": Autocomplete( "user": Autocomplete(
User, User,
@ -55,21 +49,15 @@ class WEIRegistrationForm(forms.ModelForm):
'placeholder': 'Nom ...', 'placeholder': 'Nom ...',
}, },
), ),
"birth_date": DatePickerInput(options={ "birth_date": DatePickerInput(options={'minDate': '1900-01-01',
'minDate': '1900-01-01', 'maxDate': '2100-01-01'}),
'maxDate': '2100-01-01'
}),
"deposit_check": forms.BooleanField(
required=False,
),
"deposit_type": forms.RadioSelect(),
} }
class WEIChooseBusForm(forms.Form): class WEIChooseBusForm(forms.Form):
bus = forms.ModelMultipleChoiceField( bus = forms.ModelMultipleChoiceField(
queryset=Bus.objects, queryset=Bus.objects,
label=_("Bus"), label=_("bus"),
help_text=_("This choice is not definitive. The WEI organizers are free to attribute for you a bus and a team," help_text=_("This choice is not definitive. The WEI organizers are free to attribute for you a bus and a team,"
+ " in particular if you are a free eletron."), + " in particular if you are a free eletron."),
widget=CheckboxSelectMultiple(), widget=CheckboxSelectMultiple(),
@ -84,17 +72,22 @@ class WEIChooseBusForm(forms.Form):
) )
roles = forms.ModelMultipleChoiceField( roles = forms.ModelMultipleChoiceField(
queryset=WEIRole.objects.filter(~Q(name="1A") & ~Q(name="GC WEI")), queryset=WEIRole.objects.filter(~Q(name="1A")),
label=_("WEI Roles"), label=_("WEI Roles"),
help_text=_("Select the roles that you are interested in."), help_text=_("Select the roles that you are interested in."),
initial=WEIRole.objects.filter(Q(name="Adhérent⋅e WEI") | Q(name="\u00c9lectron libre")).all(), initial=WEIRole.objects.filter(name="Adhérent⋅e WEI").all(),
widget=CheckboxSelectMultiple(), widget=CheckboxSelectMultiple(),
) )
class WEIMembershipForm(forms.ModelForm): class WEIMembershipForm(forms.ModelForm):
caution_check = forms.BooleanField(
required=False,
label=_("Caution check given"),
)
roles = forms.ModelMultipleChoiceField( roles = forms.ModelMultipleChoiceField(
queryset=WEIRole.objects.filter(~Q(name="GC WEI")), queryset=WEIRole.objects,
label=_("WEI Roles"), label=_("WEI Roles"),
widget=CheckboxSelectMultiple(), widget=CheckboxSelectMultiple(),
) )
@ -128,19 +121,6 @@ class WEIMembershipForm(forms.ModelForm):
required=False, required=False,
) )
def __init__(self, *args, wei=None, **kwargs):
super().__init__(*args, **kwargs)
if 'bus' in self.fields:
if wei is not None:
self.fields['bus'].queryset = Bus.objects.filter(wei=wei)
else:
self.fields['bus'].queryset = Bus.objects.none()
if 'team' in self.fields:
if wei is not None:
self.fields['team'].queryset = BusTeam.objects.filter(bus__wei=wei)
else:
self.fields['team'].queryset = BusTeam.objects.none()
def clean(self): def clean(self):
cleaned_data = super().clean() cleaned_data = super().clean()
if 'team' in cleaned_data and cleaned_data["team"] is not None \ if 'team' in cleaned_data and cleaned_data["team"] is not None \
@ -152,8 +132,21 @@ class WEIMembershipForm(forms.ModelForm):
model = WEIMembership model = WEIMembership
fields = ('roles', 'bus', 'team',) fields = ('roles', 'bus', 'team',)
widgets = { widgets = {
"bus": RadioSelect(), "bus": Autocomplete(
"team": RadioSelect(), Bus,
attrs={
'api_url': '/api/wei/bus/',
'placeholder': 'Bus ...',
}
),
"team": Autocomplete(
BusTeam,
attrs={
'api_url': '/api/wei/team/',
'placeholder': 'Équipe ...',
},
resetable=True,
),
} }
@ -161,7 +154,7 @@ class WEIMembership1AForm(WEIMembershipForm):
""" """
Used to confirm registrations of first year members without choosing a bus now. Used to confirm registrations of first year members without choosing a bus now.
""" """
deposit_check = None caution_check = None
roles = None roles = None
def clean(self): def clean(self):

View File

@ -2,11 +2,11 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm
from .wei2025 import WEISurvey2025 from .wei2024 import WEISurvey2024
__all__ = [ __all__ = [
'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey', 'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey',
] ]
CurrentSurvey = WEISurvey2025 CurrentSurvey = WEISurvey2024

View File

@ -121,13 +121,6 @@ class WEISurveyAlgorithm:
""" """
raise NotImplementedError raise NotImplementedError
@classmethod
def get_bus_information_form(cls):
"""
The class of the form to update the bus information.
"""
raise NotImplementedError
class WEISurvey: class WEISurvey:
""" """

View File

@ -1,347 +0,0 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import time
import json
from functools import lru_cache
from random import Random
from django import forms
from django.db import transaction
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInformation
from ...models import WEIMembership, Bus
WORDS = [
'13 organisé', '3ième mi temps', 'Années 2000', 'Apéro', 'BBQ', 'BP', 'Beauf', 'Binge drinking', 'Bon enfant',
'Cartouche', 'Catacombes', 'Chansons paillardes', 'Chansons populaires', 'Chanteur', 'Chartreuse', 'Chill',
'Core', 'DJ', 'Dancefloor', 'Danse', 'David Guetta', 'Disco', 'Eau de vie', 'Électro', 'Escalade', 'Familial',
'Fanfare', 'Fracassage', 'Féria', 'Hard rock', 'Hoeggarden', 'House', 'Huit-six', 'IPA', 'Inclusif', 'Inferno',
'Introverti', 'Jager bomb', 'Jazz', 'Jeux d\'alcool', 'Jeux de rôles', 'Jeux vidéo', 'Jul', 'Jus de fruit',
'Karaoké', 'LGBTQI+', 'Lady Gaga', 'Loup garou', 'Morning beer', 'Métal', 'Nuit blanche', 'Ovalie', 'Psychedelic',
'Pétanque', 'Rave', 'Reggae', 'Rhum', 'Ricard', 'Rock', 'Rosé', 'Rétro', 'Séducteur', 'Techno', 'Thérapie taxi',
'Théâtre', 'Trap', 'Turn up', 'Underground', 'Volley', 'Wati B', 'Zinédine Zidane',
]
class WEISurveyForm2025(forms.Form):
"""
Survey form for the year 2025.
Members choose 20 words, from which we calculate the best associated bus.
"""
word = forms.ChoiceField(
label=_("Choose a word:"),
widget=forms.RadioSelect(),
)
def set_registration(self, registration):
"""
Filter the bus selector with the buses of the current WEI.
"""
information = WEISurveyInformation2025(registration)
if not information.seed:
information.seed = int(1000 * time.time())
information.save(registration)
registration._force_save = True
registration.save()
if self.data:
self.fields["word"].choices = [(w, w) for w in WORDS]
if self.is_valid():
return
rng = Random((information.step + 1) * information.seed)
buses = WEISurveyAlgorithm2025.get_buses()
informations = {bus: WEIBusInformation2025(bus) for bus in buses}
scores = sum((list(informations[bus].scores.values()) for bus in buses), [])
if scores:
average_score = sum(scores) / len(scores)
else:
average_score = 0
preferred_words = {bus: [word for word in WORDS
if informations[bus].scores[word] >= average_score]
for bus in buses}
# Correction : proposer plusieurs mots différents à chaque étape
n_choices = 4 # Nombre de mots à proposer à chaque étape
all_preferred_words = set()
for bus_words in preferred_words.values():
all_preferred_words.update(bus_words)
all_preferred_words = list(all_preferred_words)
rng.shuffle(all_preferred_words)
words = all_preferred_words[:n_choices]
self.fields["word"].choices = [(w, w) for w in words]
class WEIBusInformation2025(WEIBusInformation):
"""
For each word, the bus has a score
"""
scores: dict
def __init__(self, bus):
self.scores = {}
for word in WORDS:
self.scores[word] = 0
super().__init__(bus)
class BusInformationForm2025(forms.ModelForm):
class Meta:
model = Bus
fields = ['information_json']
widgets = {}
def __init__(self, *args, words=None, **kwargs):
super().__init__(*args, **kwargs)
initial_scores = {}
if self.instance and self.instance.information_json:
try:
info = json.loads(self.instance.information_json)
initial_scores = info.get("scores", {})
except (json.JSONDecodeError, TypeError, AttributeError):
initial_scores = {}
if words is None:
words = WORDS
self.words = words
choices = [(i, str(i)) for i in range(6)] # [(0, '0'), (1, '1'), ..., (5, '5')]
for word in words:
self.fields[word] = forms.TypedChoiceField(
label=word,
choices=choices,
coerce=int,
initial=initial_scores.get(word, 0),
required=True,
widget=forms.RadioSelect,
help_text=_("Rate between 0 and 5."),
)
def clean(self):
cleaned_data = super().clean()
scores = {}
for word in self.words:
value = cleaned_data.get(word)
if value is not None:
scores[word] = value
# On encode en JSON
cleaned_data['information_json'] = json.dumps({"scores": scores})
return cleaned_data
class WEISurveyInformation2025(WEISurveyInformation):
"""
We store the id of the selected bus. We store only the name, but is not used in the selection:
that's only for humans that try to read data.
"""
# Random seed that is stored at the first time to ensure that words are generated only once
seed = 0
step = 0
def __init__(self, registration):
for i in range(1, 21):
setattr(self, "word" + str(i), None)
super().__init__(registration)
class WEISurvey2025(WEISurvey):
"""
Survey for the year 2025.
"""
@classmethod
def get_year(cls):
return 2025
@classmethod
def get_survey_information_class(cls):
return WEISurveyInformation2025
def get_form_class(self):
return WEISurveyForm2025
def update_form(self, form):
"""
Filter the bus selector with the buses of the WEI.
"""
form.set_registration(self.registration)
@transaction.atomic
def form_valid(self, form):
word = form.cleaned_data["word"]
self.information.step += 1
setattr(self.information, "word" + str(self.information.step), word)
self.save()
@classmethod
def get_algorithm_class(cls):
return WEISurveyAlgorithm2025
def is_complete(self) -> bool:
"""
The survey is complete once the bus is chosen.
"""
return self.information.step == 20
@classmethod
@lru_cache()
def word_mean(cls, word):
"""
Calculate the mid-score given by all buses.
"""
buses = cls.get_algorithm_class().get_buses()
return sum([cls.get_algorithm_class().get_bus_information(bus).scores[word] for bus in buses]) / buses.count()
@lru_cache()
def score(self, bus):
if not self.is_complete():
raise ValueError("Survey is not ended, can't calculate score")
bus_info = self.get_algorithm_class().get_bus_information(bus)
# Score is the given score by the bus subtracted to the mid-score of the buses.
s = sum(bus_info.scores[getattr(self.information, 'word' + str(i))]
- self.word_mean(getattr(self.information, 'word' + str(i))) for i in range(1, 21)) / 20
return s
@lru_cache()
def scores_per_bus(self):
return {bus: self.score(bus) for bus in self.get_algorithm_class().get_buses()}
@lru_cache()
def ordered_buses(self):
values = list(self.scores_per_bus().items())
values.sort(key=lambda item: -item[1])
return values
@classmethod
def clear_cache(cls):
cls.word_mean.cache_clear()
return super().clear_cache()
class WEISurveyAlgorithm2025(WEISurveyAlgorithm):
"""
The algorithm class for the year 2025.
We use Gale-Shapley algorithm to attribute 1y students into buses.
"""
@classmethod
def get_survey_class(cls):
return WEISurvey2025
@classmethod
def get_bus_information_class(cls):
return WEIBusInformation2025
@classmethod
def get_bus_information_form(cls):
return BusInformationForm2025
def run_algorithm(self, display_tqdm=False):
"""
Gale-Shapley algorithm implementation.
We modify it to allow buses to have multiple "weddings".
"""
surveys = list(self.get_survey_class()(r) for r in self.get_registrations()) # All surveys
surveys = [s for s in surveys if s.is_complete()] # Don't consider invalid surveys
# Don't manage hardcoded people
surveys = [s for s in surveys if not hasattr(s.information, 'hardcoded') or not s.information.hardcoded]
# Reset previous algorithm run
for survey in surveys:
survey.free()
survey.save()
non_men = [s for s in surveys if s.registration.gender != 'male']
men = [s for s in surveys if s.registration.gender == 'male']
quotas = {}
registrations = self.get_registrations()
non_men_total = registrations.filter(~Q(gender='male')).count()
for bus in self.get_buses():
free_seats = bus.size - WEIMembership.objects.filter(bus=bus, registration__first_year=False).count()
# Remove hardcoded people
free_seats -= WEIMembership.objects.filter(bus=bus, registration__first_year=True,
registration__information_json__icontains="hardcoded").count()
quotas[bus] = 4 + int(non_men_total / registrations.count() * free_seats)
tqdm_obj = None
if display_tqdm:
from tqdm import tqdm
tqdm_obj = tqdm(total=len(non_men), desc="Non-hommes")
# Repartition for non men people first
self.make_repartition(non_men, quotas, tqdm_obj=tqdm_obj)
quotas = {}
for bus in self.get_buses():
free_seats = bus.size - WEIMembership.objects.filter(bus=bus, registration__first_year=False).count()
free_seats -= sum(1 for s in non_men if s.information.selected_bus_pk == bus.pk)
# Remove hardcoded people
free_seats -= WEIMembership.objects.filter(bus=bus, registration__first_year=True,
registration__information_json__icontains="hardcoded").count()
quotas[bus] = free_seats
if display_tqdm:
tqdm_obj.close()
from tqdm import tqdm
tqdm_obj = tqdm(total=len(men), desc="Hommes")
self.make_repartition(men, quotas, tqdm_obj=tqdm_obj)
if display_tqdm:
tqdm_obj.close()
# Clear cache information after running algorithm
WEISurvey2025.clear_cache()
def make_repartition(self, surveys, quotas=None, tqdm_obj=None):
free_surveys = surveys.copy() # Remaining surveys
while free_surveys: # Some students are not affected
survey = free_surveys[0]
buses = survey.ordered_buses() # Preferences of the student
for bus, current_score in buses:
if self.get_bus_information(bus).has_free_seats(surveys, quotas):
# Selected bus has free places. Put student in the bus
survey.select_bus(bus)
survey.save()
free_surveys.remove(survey)
break
else:
# Current bus has not enough places. Remove the least preferred student from the bus if existing
least_preferred_survey = None
least_score = -1
# Find the least student in the bus that has a lower score than the current student
for survey2 in surveys:
if not survey2.information.valid or survey2.information.get_selected_bus() != bus:
continue
score2 = survey2.score(bus)
if current_score <= score2: # Ignore better students
continue
if least_preferred_survey is None or score2 < least_score:
least_preferred_survey = survey2
least_score = score2
if least_preferred_survey is not None:
# Remove the least student from the bus and put the current student in.
# If it does not exist, choose the next bus.
least_preferred_survey.free()
least_preferred_survey.save()
free_surveys.append(least_preferred_survey)
survey.select_bus(bus)
survey.save()
free_surveys.remove(survey)
break
else:
raise ValueError(f"User {survey.registration.user} has no free seat")
if tqdm_obj is not None:
tqdm_obj.n = len(surveys) - len(free_surveys)
tqdm_obj.refresh()

View File

@ -1,18 +0,0 @@
# Generated by Django 4.2.21 on 2025-05-25 12:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wei', '0010_remove_weiregistration_specific_diet'),
]
operations = [
migrations.AlterField(
model_name='weiclub',
name='year',
field=models.PositiveIntegerField(default=2025, unique=True, verbose_name='year'),
),
]

View File

@ -1,20 +0,0 @@
# Generated by Django 4.2.21 on 2025-05-29 16:16
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('member', '0014_create_bda'),
('wei', '0011_alter_weiclub_year'),
]
operations = [
migrations.AddField(
model_name='bus',
name='club',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bus', to='member.club', verbose_name='club'),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 4.2.21 on 2025-06-01 21:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wei', '0012_bus_club'),
]
operations = [
migrations.AddField(
model_name='weiclub',
name='caution_amount',
field=models.PositiveIntegerField(default=0, verbose_name='caution amount'),
),
migrations.AddField(
model_name='weiregistration',
name='caution_type',
field=models.CharField(choices=[('check', 'Check'), ('note', 'Note transaction')], default='check', max_length=16, verbose_name='caution type'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 4.2.23 on 2025-07-15 14:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wei', '0013_weiclub_caution_amount_weiregistration_caution_type'),
]
operations = [
migrations.AddField(
model_name='weiclub',
name='fee_soge_credit',
field=models.PositiveIntegerField(default=2000, verbose_name='fee soge credit'),
),
]

View File

@ -1,40 +0,0 @@
# Generated by Django 4.2.23 on 2025-07-15 16:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wei', '0014_weiclub_fee_soge_credit'),
]
operations = [
migrations.RemoveField(
model_name='weiclub',
name='caution_amount',
),
migrations.RemoveField(
model_name='weiregistration',
name='caution_check',
),
migrations.RemoveField(
model_name='weiregistration',
name='caution_type',
),
migrations.AddField(
model_name='weiclub',
name='deposit_amount',
field=models.PositiveIntegerField(default=0, verbose_name='deposit amount'),
),
migrations.AddField(
model_name='weiregistration',
name='deposit_check',
field=models.BooleanField(default=False, verbose_name='Deposit check given'),
),
migrations.AddField(
model_name='weiregistration',
name='deposit_type',
field=models.CharField(choices=[('check', 'Check'), ('note', 'Note transaction')], default='check', max_length=16, verbose_name='deposit type'),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 5.2.4 on 2025-07-19 12:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wei', '0015_remove_weiclub_caution_amount_and_more'),
]
operations = [
migrations.AddField(
model_name='weiregistration',
name='fee',
field=models.PositiveIntegerField(blank=True, default=0, verbose_name='fee'),
),
migrations.AlterField(
model_name='weiclub',
name='fee_soge_credit',
field=models.PositiveIntegerField(default=2000, verbose_name='membership fee (soge credit)'),
),
]

View File

@ -33,16 +33,6 @@ class WEIClub(Club):
verbose_name=_("date end"), verbose_name=_("date end"),
) )
deposit_amount = models.PositiveIntegerField(
verbose_name=_("deposit amount"),
default=0,
)
fee_soge_credit = models.PositiveIntegerField(
verbose_name=_("membership fee (soge credit)"),
default=2000,
)
class Meta: class Meta:
verbose_name = _("WEI") verbose_name = _("WEI")
verbose_name_plural = _("WEI") verbose_name_plural = _("WEI")
@ -82,15 +72,6 @@ class Bus(models.Model):
default=50, default=50,
) )
club = models.OneToOneField(
Club,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="bus",
verbose_name=_("club"),
)
description = models.TextField( description = models.TextField(
blank=True, blank=True,
default="", default="",
@ -202,19 +183,9 @@ class WEIRegistration(models.Model):
verbose_name=_("Credit from Société générale"), verbose_name=_("Credit from Société générale"),
) )
deposit_check = models.BooleanField( caution_check = models.BooleanField(
default=False, default=False,
verbose_name=_("Deposit check given") verbose_name=_("Caution check given")
)
deposit_type = models.CharField(
max_length=16,
choices=(
('check', _("Check")),
('note', _("Note transaction")),
),
default='check',
verbose_name=_("deposit type"),
) )
birth_date = models.DateField( birth_date = models.DateField(
@ -285,12 +256,6 @@ class WEIRegistration(models.Model):
"encoded in JSON"), "encoded in JSON"),
) )
fee = models.PositiveIntegerField(
default=0,
verbose_name=_('fee'),
blank=True,
)
class Meta: class Meta:
unique_together = ('user', 'wei',) unique_together = ('user', 'wei',)
verbose_name = _("WEI User") verbose_name = _("WEI User")
@ -315,25 +280,7 @@ class WEIRegistration(models.Model):
self.information_json = json.dumps(information, indent=2) self.information_json = json.dumps(information, indent=2)
@property @property
def is_validated(self): def fee(self):
try:
return self.membership is not None
except AttributeError:
return False
@property
def validation_status(self):
"""
Define an order to have easier access to validatable registrations
"""
if self.fee + (self.wei.deposit_amount if self.deposit_type == 'note' else 0) > self.user.note.balance:
return 2
elif self.first_year:
return 1
else:
return 0
def calculate_fee(self):
bde = Club.objects.get(pk=1) bde = Club.objects.get(pk=1)
kfet = Club.objects.get(pk=2) kfet = Club.objects.get(pk=2)
@ -348,8 +295,7 @@ class WEIRegistration(models.Model):
date_start__gte=bde.membership_start, date_start__gte=bde.membership_start,
).exists() ).exists()
fee = self.wei.fee_soge_credit if self.soge_credit \ fee = self.wei.membership_fee_paid if self.user.profile.paid \
else self.wei.membership_fee_paid if self.user.profile.paid \
else self.wei.membership_fee_unpaid else self.wei.membership_fee_unpaid
if not kfet_member: if not kfet_member:
fee += kfet.membership_fee_paid if self.user.profile.paid \ fee += kfet.membership_fee_paid if self.user.profile.paid \
@ -360,9 +306,12 @@ class WEIRegistration(models.Model):
return fee return fee
def save(self, *args, **kwargs): @property
self.fee = self.calculate_fee() def is_validated(self):
super().save(*args, **kwargs) try:
return self.membership is not None
except AttributeError:
return False
class WEIMembership(Membership): class WEIMembership(Membership):

View File

@ -58,8 +58,8 @@ class WEIRegistrationTable(tables.Table):
validate = tables.Column( validate = tables.Column(
verbose_name=_("Validate"), verbose_name=_("Validate"),
orderable=True, orderable=False,
accessor='validate_status', accessor=A('pk'),
attrs={ attrs={
'th': { 'th': {
'id': 'validate-membership-header' 'id': 'validate-membership-header'
@ -100,11 +100,10 @@ class WEIRegistrationTable(tables.Table):
url = reverse_lazy('wei:validate_registration', args=(record.pk,)) url = reverse_lazy('wei:validate_registration', args=(record.pk,))
text = _('Validate') text = _('Validate')
status = record.validation_status if record.fee > record.user.note.balance and not record.soge_credit:
if status == 2:
btn_class = 'btn-secondary' btn_class = 'btn-secondary'
tooltip = _("The user does not have enough money.") tooltip = _("The user does not have enough money.")
elif status == 1: elif record.first_year:
btn_class = 'btn-info' btn_class = 'btn-info'
tooltip = _("The user is in first year. You may validate the credit, the algorithm will run later.") tooltip = _("The user is in first year. You may validate the credit, the algorithm will run later.")
else: else:
@ -122,10 +121,9 @@ class WEIRegistrationTable(tables.Table):
attrs = { attrs = {
'class': 'table table-condensed table-striped table-hover' 'class': 'table table-condensed table-striped table-hover'
} }
order_by = ('validate', 'user',)
model = WEIRegistration model = WEIRegistration
template_name = 'django_tables2/bootstrap4.html' template_name = 'django_tables2/bootstrap4.html'
fields = ('user', 'user__first_name', 'user__last_name', 'first_year', 'deposit_check', fields = ('user', 'user__first_name', 'user__last_name', 'first_year', 'caution_check',
'edit', 'validate', 'delete',) 'edit', 'validate', 'delete',)
row_attrs = { row_attrs = {
'class': 'table-row', 'class': 'table-row',
@ -165,7 +163,7 @@ class WEIMembershipTable(tables.Table):
model = WEIMembership model = WEIMembership
template_name = 'django_tables2/bootstrap4.html' template_name = 'django_tables2/bootstrap4.html'
fields = ('user', 'user__last_name', 'user__first_name', 'registration__gender', 'user__profile__department', fields = ('user', 'user__last_name', 'user__first_name', 'registration__gender', 'user__profile__department',
'year', 'bus', 'team', 'registration__deposit_check', ) 'year', 'bus', 'team', 'registration__caution_check', )
row_attrs = { row_attrs = {
'class': 'table-row', 'class': 'table-row',
'id': lambda record: "row-" + str(record.pk), 'id': lambda record: "row-" + str(record.pk),

View File

@ -40,20 +40,22 @@ SPDX-License-Identifier: GPL-3.0-or-later
<dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_fee_paid|pretty_money }}</dd> <dd class="col-xl-6">{{ club.membership_fee_paid|pretty_money }}</dd>
{% else %} {% else %}
{% with bde_kfet_fee=club.parent_club.membership_fee_paid|add:club.parent_club.parent_club.membership_fee_paid %}
<dt class="col-xl-6">{% trans 'WEI fee (paid students)'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'WEI fee (paid students)'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_fee_paid|pretty_money }} <dd class="col-xl-6">{{ club.membership_fee_paid|add:bde_kfet_fee|pretty_money }}
<i class="fa fa-question-circle"
title="{% trans "The BDE membership is included in the WEI registration." %}"></i></dd>
{% endwith %}
{% with bde_kfet_fee=club.parent_club.membership_fee_unpaid|add:club.parent_club.parent_club.membership_fee_unpaid %}
<dt class="col-xl-6">{% trans 'WEI fee (unpaid students)'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'WEI fee (unpaid students)'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_fee_unpaid|pretty_money }} <dd class="col-xl-6">{{ club.membership_fee_unpaid|add:bde_kfet_fee|pretty_money }}
<i class="fa fa-question-circle"
title="{% trans "The BDE membership is included in the WEI registration." %}"></i></dd>
{% endwith %}
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if club.deposit_amount > 0 %}
<dt class="col-xl-6">{% trans 'Deposit amount'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.deposit_amount|pretty_money }}</dd>
{% endif %}
{% if "note.view_note"|has_perm:club.note %} {% if "note.view_note"|has_perm:club.note %}
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.note.balance | pretty_money }}</dd> <dd class="col-xl-6">{{ club.note.balance | pretty_money }}</dd>

View File

@ -16,14 +16,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
<div class="card-footer text-center"> <div class="card-footer text-center">
{% if object.club %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_detail' pk=object.club.pk %}"
data-turbolinks="false">{% trans "View club" %}</a>
{% endif %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus' pk=object.pk %}" <a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus' pk=object.pk %}"
data-turbolinks="false">{% trans "Edit" %}</a> data-turbolinks="false">{% trans "Edit" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus_info' pk=object.pk %}"
data-turbolinks="false">{% trans "Edit information" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_team' pk=object.pk %}" <a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_team' pk=object.pk %}"
data-turbolinks="false">{% trans "Add team" %}</a> data-turbolinks="false">{% trans "Add team" %}</a>
</div> </div>

View File

@ -18,8 +18,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card-footer text-center"> <div class="card-footer text-center">
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus' pk=bus.pk %}" <a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus' pk=bus.pk %}"
data-turbolinks="false">{% trans "Edit" %}</a> data-turbolinks="false">{% trans "Edit" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:manage_bus' pk=bus.pk %}"
data-turbolinks="false">{% trans "View" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_team' pk=bus.pk %}" <a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_team' pk=bus.pk %}"
data-turbolinks="false">{% trans "Add team" %}</a> data-turbolinks="false">{% trans "Add team" %}</a>
</div> </div>

View File

@ -13,17 +13,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card-body"> <div class="card-body">
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{{ form.media }}
{{ form|crispy }} {{ form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button> <button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
</form> </form>
</div> </div>
</div> </div>
<script>
document.addEventListener("DOMContentLoaded", function () {
if (window.jscolor && jscolor.install) {
jscolor.install();
}
});
</script>
{% endblock %} {% endblock %}

View File

@ -67,6 +67,20 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
{% endif %} {% endif %}
{% if history_list.data %}
<div class="card bg-white mb-3">
<div class="card-header position-relative" id="historyListHeading">
<a class="stretched-link font-weight-bold text-decoration-none" {% if "note.view_note"|has_perm:club.note %}
href="{% url 'note:transactions' pk=club.note.pk %}" {% endif %}>
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a>
</div>
<div id="history_list">
{% render_table history_list %}
</div>
</div>
{% endif %}
{% if pre_registrations.data %} {% if pre_registrations.data %}
<div class="card bg-white mb-3"> <div class="card bg-white mb-3">
<div class="card-header position-relative" id="historyListHeading"> <div class="card-header position-relative" id="historyListHeading">
@ -81,24 +95,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
{% endif %} {% endif %}
{% if can_validate_1a %} {% if can_validate_1a %}
<a href="{% url 'wei:wei_1A_list' pk=object.pk %}" class="btn btn-block btn-info">{% trans "Attribute buses" %}</a> <a href="{% url 'wei:wei_1A_list' pk=object.pk %}" class="btn btn-block btn-info">{% trans "Attribute buses" %}</a>
{% endif %} {% endif %}
{% if history_list.data %}
<div class="card bg-white mt-3">
<div class="card-header position-relative" id="historyListHeading">
<a class="stretched-link font-weight-bold text-decoration-none" {% if "note.view_note"|has_perm:club.note %}
href="{% url 'note:transactions' pk=club.note.pk %}" {% endif %}>
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a>
</div>
<div id="history_list">
{% render_table history_list %}
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}
{% block extrajavascript %} {% block extrajavascript %}

View File

@ -95,8 +95,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
<dd class="col-xl-6"><em>{% trans "The algorithm didn't run." %}</em></dd> <dd class="col-xl-6"><em>{% trans "The algorithm didn't run." %}</em></dd>
{% endif %} {% endif %}
{% else %} {% else %}
<dt class="col-xl-6">{% trans 'Deposit check given'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'caution check given'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.deposit_check|yesno }}</dd> <dd class="col-xl-6">{{ registration.caution_check|yesno }}</dd>
{% with information=registration.information %} {% with information=registration.information %}
<dt class="col-xl-6">{% trans 'preferred bus'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'preferred bus'|capfirst %}</dt>
@ -137,37 +137,31 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if registration.soge_credit %} {% if registration.soge_credit %}
<div class="alert alert-warning"> <div class="alert alert-warning">
{% blocktrans trimmed %} {% blocktrans trimmed %}
The WEI will partially be paid by Société générale. The membership will be created even if the bank didn't pay the BDE yet. The WEI will be paid by Société générale. The membership will be created even if the bank didn't pay the BDE yet.
The membership transaction will be created but will be invalid. You will have to validate it once the bank The membership transaction will be created but will be invalid. You will have to validate it once the bank
validated the creation of the account, or to change the payment method. validated the creation of the account, or to change the payment method.
{% endblocktrans %} {% endblocktrans %}
</div> </div>
{% endif %}
<div class="alert {% if registration.user.note.balance < fee %}alert-danger{% else %}alert-success{% endif %}">
<h5>{% trans "Required payments:" %}</h5>
<ul>
<li>{% blocktrans trimmed with amount=fee|pretty_money %}
Membership fees: {{ amount }}
{% endblocktrans %}</li>
{% if registration.deposit_type == 'note' %}
<li>{% blocktrans trimmed with amount=club.deposit_amount|pretty_money %}
Deposit (by Note transaction): {{ amount }}
{% endblocktrans %}</li>
{% else %} {% else %}
<li>{% blocktrans trimmed with amount=club.deposit_amount|pretty_money %} {% if registration.user.note.balance < fee %}
Deposit (by check): {{ amount }} <div class="alert alert-danger">
{% endblocktrans %}</li> {% with pretty_fee=fee|pretty_money %}
{% endif %} {% blocktrans trimmed with balance=registration.user.note.balance|pretty_money %}
<li><strong>{% blocktrans trimmed with total=total_needed|pretty_money %} The note don't have enough money ({{ balance }}, {{ pretty_fee }} required).
Total needed: {{ total }} The registration may fail if you don't credit the note now.
{% endblocktrans %}</strong></li> {% endblocktrans %}
</ul> {% endwith %}
<p>{% blocktrans trimmed with balance=registration.user.note.balance|pretty_money %}
Current balance: {{ balance }}
{% endblocktrans %}</p>
</div> </div>
{% else %}
<div class="alert alert-success">
{% blocktrans trimmed with pretty_fee=fee|pretty_money %}
The note has enough money ({{ pretty_fee }} required), the registration is possible.
{% endblocktrans %}
</div>
{% endif %}
{% endif %}
{% if not registration.deposit_check and not registration.first_year and registration.caution_type == 'check' %} {% if not registration.caution_check and not registration.first_year %}
<div class="alert alert-danger"> <div class="alert alert-danger">
{% trans "The user didn't give her/his caution check." %} {% trans "The user didn't give her/his caution check." %}
</div> </div>
@ -206,27 +200,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
} }
} }
</script> </script>
<script>
$(document).ready(function () {
function refreshTeams() {
let buses = [];
$("input[name='bus']:checked").each(function (ignored) {
buses.push($(this).parent().text().trim());
});
console.log(buses);
$("input[name='team']").each(function () {
let label = $(this).parent();
$(this).parent().addClass('d-none');
buses.forEach(function (bus) {
if (label.text().includes(bus))
label.removeClass('d-none');
});
});
}
$("input[name='bus']").change(refreshTeams);
refreshTeams();
});
</script>
{% endblock %} {% endblock %}

View File

@ -6,6 +6,8 @@ from datetime import date, timedelta
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import TestCase from django.test import TestCase
from django.urls import reverse
from note.models import NoteUser
from ..forms.surveys.wei2024 import WEIBusInformation2024, WEISurvey2024, WORDS, WEISurveyInformation2024 from ..forms.surveys.wei2024 import WEIBusInformation2024, WEISurvey2024, WORDS, WEISurveyInformation2024
from ..models import Bus, WEIClub, WEIRegistration from ..models import Bus, WEIClub, WEIRegistration
@ -127,3 +129,44 @@ class TestWEIAlgorithm(TestCase):
self.assertLessEqual(max_score - score, 25) # Always less than 25 % of tolerance self.assertLessEqual(max_score - score, 25) # Always less than 25 % of tolerance
self.assertLessEqual(penalty / 100, 25) # Tolerance of 5 % self.assertLessEqual(penalty / 100, 25) # Tolerance of 5 %
def test_register_1a(self):
"""
Test register a first year member to the WEI and complete the survey
"""
response = self.client.get(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk)))
self.assertEqual(response.status_code, 200)
user = User.objects.create(username="toto", email="toto@example.com")
NoteUser.objects.create(user=user)
response = self.client.post(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk)), dict(
user=user.id,
soge_credit=True,
birth_date=date(2000, 1, 1),
gender='nonbinary',
clothing_cut='female',
clothing_size='XS',
health_issues='I am a bot',
emergency_contact_name='NoteKfet2020',
emergency_contact_phone='+33123456789',
))
qs = WEIRegistration.objects.filter(user_id=user.id)
self.assertTrue(qs.exists())
registration = qs.get()
self.assertRedirects(response, reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), 302, 200)
for question in WORDS:
# Fill 1A Survey, 10 pages
# be careful if questionnary form change (number of page, type of answer...)
response = self.client.post(reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), {
question: "1"
})
registration.refresh_from_db()
survey = WEISurvey2024(registration)
self.assertRedirects(response, reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), 302,
302 if survey.is_complete() else 200)
self.assertIsNotNone(getattr(survey.information, question), "Survey page " + question + " failed")
survey = WEISurvey2024(registration)
self.assertTrue(survey.is_complete())
survey.select_bus(self.buses[0])
survey.save()
self.assertIsNotNone(survey.information.get_selected_bus())

View File

@ -1,111 +0,0 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import random
from django.contrib.auth.models import User
from django.test import TestCase
from ..forms.surveys.wei2025 import WEIBusInformation2025, WEISurvey2025, WORDS, WEISurveyInformation2025
from ..models import Bus, WEIClub, WEIRegistration
class TestWEIAlgorithm(TestCase):
"""
Run some tests to ensure that the WEI algorithm is working well.
"""
fixtures = ('initial',)
def setUp(self):
"""
Create some test data, with one WEI and 10 buses with random score attributions.
"""
self.wei = WEIClub.objects.create(
name="WEI 2025",
email="wei2025@example.com",
date_start='2025-09-12',
date_end='2025-09-14',
year=2025,
membership_start='2025-06-01'
)
self.buses = []
for i in range(10):
bus = Bus.objects.create(wei=self.wei, name=f"Bus {i}", size=10)
self.buses.append(bus)
information = WEIBusInformation2025(bus)
for word in WORDS:
information.scores[word] = random.randint(0, 101)
information.save()
bus.save()
def test_survey_algorithm_small(self):
"""
There are only a few people in each bus, ensure that each person has its best bus
"""
# Add a few users
for i in range(10):
user = User.objects.create(username=f"user{i}")
registration = WEIRegistration.objects.create(
user=user,
wei=self.wei,
first_year=True,
birth_date='2000-01-01',
)
information = WEISurveyInformation2025(registration)
for j in range(1, 21):
setattr(information, f'word{j}', random.choice(WORDS))
information.step = 20
information.save(registration)
registration.save()
# Run algorithm
WEISurvey2025.get_algorithm_class()().run_algorithm()
# Ensure that everyone has its first choice
for r in WEIRegistration.objects.filter(wei=self.wei).all():
survey = WEISurvey2025(r)
preferred_bus = survey.ordered_buses()[0][0]
chosen_bus = survey.information.get_selected_bus()
self.assertEqual(preferred_bus, chosen_bus)
def test_survey_algorithm_full(self):
"""
Buses are full of first year people, ensure that they are happy
"""
# Add a lot of users
for i in range(95):
user = User.objects.create(username=f"user{i}")
registration = WEIRegistration.objects.create(
user=user,
wei=self.wei,
first_year=True,
birth_date='2000-01-01',
)
information = WEISurveyInformation2025(registration)
for j in range(1, 21):
setattr(information, f'word{j}', random.choice(WORDS))
information.step = 20
information.save(registration)
registration.save()
# Run algorithm
WEISurvey2025.get_algorithm_class()().run_algorithm()
penalty = 0
# Ensure that everyone seems to be happy
# We attribute a penalty for each user that didn't have its first choice
# The penalty is the square of the distance between the score of the preferred bus
# and the score of the attributed bus
# We consider it acceptable if the mean of this distance is lower than 5 %
for r in WEIRegistration.objects.filter(wei=self.wei).all():
survey = WEISurvey2025(r)
chosen_bus = survey.information.get_selected_bus()
buses = survey.ordered_buses()
score = min(v for bus, v in buses if bus == chosen_bus)
max_score = buses[0][1]
penalty += (max_score - score) ** 2
self.assertLessEqual(max_score - score, 25) # Always less than 25 % of tolerance
self.assertLessEqual(penalty / 100, 25) # Tolerance of 5 %

View File

@ -101,7 +101,7 @@ class TestWEIRegistration(TestCase):
user_id=self.user.id, user_id=self.user.id,
wei_id=self.wei.id, wei_id=self.wei.id,
soge_credit=True, soge_credit=True,
deposit_check=True, caution_check=True,
birth_date=date(2000, 1, 1), birth_date=date(2000, 1, 1),
gender="nonbinary", gender="nonbinary",
clothing_cut="male", clothing_cut="male",
@ -121,13 +121,11 @@ class TestWEIRegistration(TestCase):
email="gc.wei@example.com", email="gc.wei@example.com",
membership_fee_paid=12500, membership_fee_paid=12500,
membership_fee_unpaid=5500, membership_fee_unpaid=5500,
fee_soge_credit=2000,
membership_start=str(self.year + 1) + "-08-01", membership_start=str(self.year + 1) + "-08-01",
membership_end=str(self.year + 1) + "-09-30", membership_end=str(self.year + 1) + "-09-30",
year=self.year + 1, year=self.year + 1,
date_start=str(self.year + 1) + "-09-01", date_start=str(self.year + 1) + "-09-01",
date_end=str(self.year + 1) + "-09-03", date_end=str(self.year + 1) + "-09-03",
deposit_amount=12000,
)) ))
qs = WEIClub.objects.filter(name="Create WEI Test", year=self.year + 1) qs = WEIClub.objects.filter(name="Create WEI Test", year=self.year + 1)
self.assertTrue(qs.exists()) self.assertTrue(qs.exists())
@ -158,12 +156,10 @@ class TestWEIRegistration(TestCase):
email="wei-updated@example.com", email="wei-updated@example.com",
membership_fee_paid=0, membership_fee_paid=0,
membership_fee_unpaid=0, membership_fee_unpaid=0,
fee_soge_credit=0,
membership_start="2000-08-01", membership_start="2000-08-01",
membership_end="2000-09-30", membership_end="2000-09-30",
date_start="2000-09-01", date_start="2000-09-01",
date_end="2000-09-03", date_end="2000-09-03",
deposit_amount=12000,
)) ))
qs = WEIClub.objects.filter(name="Update WEI Test", id=self.wei.id) qs = WEIClub.objects.filter(name="Update WEI Test", id=self.wei.id)
self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=self.wei.pk)), 302, 200) self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=self.wei.pk)), 302, 200)
@ -322,7 +318,6 @@ class TestWEIRegistration(TestCase):
bus=[], bus=[],
team=[], team=[],
roles=[], roles=[],
deposit_type='check'
)) ))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertFalse(response.context["membership_form"].is_valid()) self.assertFalse(response.context["membership_form"].is_valid())
@ -339,8 +334,7 @@ class TestWEIRegistration(TestCase):
emergency_contact_phone='+33123456789', emergency_contact_phone='+33123456789',
bus=[self.bus.id], bus=[self.bus.id],
team=[self.team.id], team=[self.team.id],
roles=[role.id for role in WEIRole.objects.filter(~Q(name="1A") & ~Q(name="GC WEI")).all()], roles=[role.id for role in WEIRole.objects.filter(~Q(name="1A")).all()],
deposit_type='check'
)) ))
qs = WEIRegistration.objects.filter(user_id=user.id) qs = WEIRegistration.objects.filter(user_id=user.id)
self.assertTrue(qs.exists()) self.assertTrue(qs.exists())
@ -360,7 +354,6 @@ class TestWEIRegistration(TestCase):
bus=[self.bus.id], bus=[self.bus.id],
team=[self.team.id], team=[self.team.id],
roles=[role.id for role in WEIRole.objects.filter(~Q(name="1A")).all()], roles=[role.id for role in WEIRole.objects.filter(~Q(name="1A")).all()],
deposit_type='check'
)) ))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertTrue("This user is already registered to this WEI." in str(response.context["form"].errors)) self.assertTrue("This user is already registered to this WEI." in str(response.context["form"].errors))
@ -513,12 +506,11 @@ class TestWEIRegistration(TestCase):
team=[self.team.id], team=[self.team.id],
roles=[role.id for role in WEIRole.objects.filter(name="Adhérent⋅e WEI").all()], roles=[role.id for role in WEIRole.objects.filter(name="Adhérent⋅e WEI").all()],
information_json=self.registration.information_json, information_json=self.registration.information_json,
deposit_type='check'
) )
) )
qs = WEIRegistration.objects.filter(user_id=self.user.id, soge_credit=False, clothing_size="M") qs = WEIRegistration.objects.filter(user_id=self.user.id, soge_credit=False, clothing_size="M")
self.assertTrue(qs.exists()) self.assertTrue(qs.exists())
self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=qs.get().wei.pk)), 302, 200) self.assertRedirects(response, reverse("wei:validate_registration", kwargs=dict(pk=qs.get().pk)), 302, 200)
# Check the page when the registration is already validated # Check the page when the registration is already validated
membership = WEIMembership( membership = WEIMembership(
@ -568,12 +560,11 @@ class TestWEIRegistration(TestCase):
team=[self.team.id], team=[self.team.id],
roles=[role.id for role in WEIRole.objects.filter(name="Adhérent⋅e WEI").all()], roles=[role.id for role in WEIRole.objects.filter(name="Adhérent⋅e WEI").all()],
information_json=self.registration.information_json, information_json=self.registration.information_json,
deposit_type='check'
) )
) )
qs = WEIRegistration.objects.filter(user_id=self.user.id, clothing_size="L") qs = WEIRegistration.objects.filter(user_id=self.user.id, clothing_size="L")
self.assertTrue(qs.exists()) self.assertTrue(qs.exists())
self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=qs.get().wei.pk)), 302, 200) self.assertRedirects(response, reverse("wei:validate_registration", kwargs=dict(pk=qs.get().pk)), 302, 200)
# Test invalid form # Test invalid form
response = self.client.post( response = self.client.post(
@ -592,7 +583,6 @@ class TestWEIRegistration(TestCase):
team=[], team=[],
roles=[], roles=[],
information_json=self.registration.information_json, information_json=self.registration.information_json,
deposit_type='check'
) )
) )
self.assertFalse(response.context["membership_form"].is_valid()) self.assertFalse(response.context["membership_form"].is_valid())
@ -634,7 +624,7 @@ class TestWEIRegistration(TestCase):
second_bus = Bus.objects.create(wei=self.wei, name="Second bus") second_bus = Bus.objects.create(wei=self.wei, name="Second bus")
second_team = BusTeam.objects.create(bus=second_bus, name="Second team", color=42) second_team = BusTeam.objects.create(bus=second_bus, name="Second team", color=42)
response = self.client.post(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk)), dict( response = self.client.post(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk)), dict(
roles=[WEIRole.objects.get(name="Adhérent⋅e WEI").id], roles=[WEIRole.objects.get(name="GC WEI").id],
bus=self.bus.pk, bus=self.bus.pk,
team=second_team.pk, team=second_team.pk,
credit_type=4, # Bank transfer credit_type=4, # Bank transfer
@ -642,14 +632,13 @@ class TestWEIRegistration(TestCase):
last_name="admin", last_name="admin",
first_name="admin", first_name="admin",
bank="Société générale", bank="Société générale",
deposit_check=True,
)) ))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertFalse(response.context["form"].is_valid()) self.assertFalse(response.context["form"].is_valid())
self.assertTrue("This team doesn&#x27;t belong to the given bus." in str(response.context["form"].errors)) self.assertTrue("This team doesn&#x27;t belong to the given bus." in str(response.context["form"].errors))
response = self.client.post(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk)), dict( response = self.client.post(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk)), dict(
roles=[WEIRole.objects.get(name="Adhérent⋅e WEI").id], roles=[WEIRole.objects.get(name="GC WEI").id],
bus=self.bus.pk, bus=self.bus.pk,
team=self.team.pk, team=self.team.pk,
credit_type=4, # Bank transfer credit_type=4, # Bank transfer
@ -657,10 +646,8 @@ class TestWEIRegistration(TestCase):
last_name="admin", last_name="admin",
first_name="admin", first_name="admin",
bank="Société générale", bank="Société générale",
deposit_check=True,
)) ))
self.assertRedirects(response, reverse("wei:wei_registrations", kwargs=dict(pk=self.registration.wei.pk)), 302, 200) self.assertRedirects(response, reverse("wei:wei_registrations", kwargs=dict(pk=self.registration.wei.pk)), 302, 200)
# Check if the membership is successfully created # Check if the membership is successfully created
membership = WEIMembership.objects.filter(user_id=self.user.id, club=self.wei) membership = WEIMembership.objects.filter(user_id=self.user.id, club=self.wei)
self.assertTrue(membership.exists()) self.assertTrue(membership.exists())
@ -680,7 +667,11 @@ class TestWEIRegistration(TestCase):
self.assertTrue(soge_credit.exists()) self.assertTrue(soge_credit.exists())
soge_credit = soge_credit.get() soge_credit = soge_credit.get()
self.assertTrue(membership.transaction in soge_credit.transactions.all()) self.assertTrue(membership.transaction in soge_credit.transactions.all())
self.assertTrue(kfet_membership.transaction in soge_credit.transactions.all())
self.assertTrue(bde_membership.transaction in soge_credit.transactions.all())
self.assertFalse(membership.transaction.valid) self.assertFalse(membership.transaction.valid)
self.assertFalse(kfet_membership.transaction.valid)
self.assertFalse(bde_membership.transaction.valid)
# Check that if the WEI is started, we can't update a wei # Check that if the WEI is started, we can't update a wei
self.wei.date_start = date(2000, 1, 1) self.wei.date_start = date(2000, 1, 1)
@ -776,7 +767,7 @@ class TestDefaultWEISurvey(TestCase):
WEISurvey.update_form(None, None) WEISurvey.update_form(None, None)
self.assertEqual(CurrentSurvey.get_algorithm_class().get_survey_class(), CurrentSurvey) self.assertEqual(CurrentSurvey.get_algorithm_class().get_survey_class(), CurrentSurvey)
self.assertEqual(CurrentSurvey.get_year(), 2025) self.assertEqual(CurrentSurvey.get_year(), 2024)
class TestWeiAPI(TestAPI): class TestWeiAPI(TestAPI):
@ -813,7 +804,7 @@ class TestWeiAPI(TestAPI):
user_id=self.user.id, user_id=self.user.id,
wei_id=self.wei.id, wei_id=self.wei.id,
soge_credit=True, soge_credit=True,
deposit_check=True, caution_check=True,
birth_date=date(2000, 1, 1), birth_date=date(2000, 1, 1),
gender="nonbinary", gender="nonbinary",
clothing_cut="male", clothing_cut="male",

View File

@ -4,7 +4,7 @@
from django.urls import path from django.urls import path
from .views import CurrentWEIDetailView, WEI1AListView, WEIListView, WEICreateView, WEIDetailView, WEIUpdateView, \ from .views import CurrentWEIDetailView, WEI1AListView, WEIListView, WEICreateView, WEIDetailView, WEIUpdateView, \
WEIRegistrationsView, WEIMembershipsView, MemberListRenderView, BusInformationUpdateView, \ WEIRegistrationsView, WEIMembershipsView, MemberListRenderView, \
BusCreateView, BusManageView, BusUpdateView, BusTeamCreateView, BusTeamManageView, BusTeamUpdateView, \ BusCreateView, BusManageView, BusUpdateView, BusTeamCreateView, BusTeamManageView, BusTeamUpdateView, \
WEIAttributeBus1AView, WEIAttributeBus1ANextView, WEIRegister1AView, WEIRegister2AView, WEIUpdateRegistrationView, \ WEIAttributeBus1AView, WEIAttributeBus1ANextView, WEIRegister1AView, WEIRegister2AView, WEIUpdateRegistrationView, \
WEIDeleteRegistrationView, WEIValidateRegistrationView, WEISurveyView, WEISurveyEndView, WEIClosedView WEIDeleteRegistrationView, WEIValidateRegistrationView, WEISurveyView, WEISurveyEndView, WEIClosedView
@ -42,5 +42,4 @@ urlpatterns = [
path('detail/<int:pk>/closed/', WEIClosedView.as_view(), name="wei_closed"), path('detail/<int:pk>/closed/', WEIClosedView.as_view(), name="wei_closed"),
path('bus-1A/<int:pk>/', WEIAttributeBus1AView.as_view(), name="wei_bus_1A"), path('bus-1A/<int:pk>/', WEIAttributeBus1AView.as_view(), name="wei_bus_1A"),
path('bus-1A/next/<int:pk>/', WEIAttributeBus1ANextView.as_view(), name="wei_bus_1A_next"), path('bus-1A/next/<int:pk>/', WEIAttributeBus1ANextView.as_view(), name="wei_bus_1A_next"),
path('update-bus-info/<int:pk>/', BusInformationUpdateView.as_view(), name="update_bus_info"),
] ]

View File

@ -4,18 +4,16 @@
import os import os
import shutil import shutil
import subprocess import subprocess
from datetime import date from datetime import date, timedelta
from tempfile import mkdtemp from tempfile import mkdtemp
from django.conf import settings from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import transaction from django.db import transaction
from django.db.models import Q, Count, Case, When, Value, IntegerField, F from django.db.models import Q, Count
from django.db.models.functions.text import Lower from django.db.models.functions.text import Lower
from django import forms
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404
from django.shortcuts import redirect from django.shortcuts import redirect
from django.template.loader import render_to_string from django.template.loader import render_to_string
@ -27,7 +25,7 @@ from django.views.generic.edit import BaseFormView, DeleteView
from django_tables2 import SingleTableView, MultiTableMixin from django_tables2 import SingleTableView, MultiTableMixin
from api.viewsets import is_regex from api.viewsets import is_regex
from member.models import Membership, Club from member.models import Membership, Club
from note.models import Transaction, NoteClub, Alias, SpecialTransaction from note.models import Transaction, NoteClub, Alias, SpecialTransaction, NoteSpecial
from note.tables import HistoryTable from note.tables import HistoryTable
from note_kfet.settings import BASE_DIR from note_kfet.settings import BASE_DIR
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
@ -133,23 +131,6 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, D
membership=None, membership=None,
wei=club wei=club
) )
# Annotate the query to be able to sort registrations on validate status
pre_registrations = pre_registrations.annotate(
deposit=Case(
When(deposit_type='note', then=F('wei__deposit_amount')),
default=Value(0),
output_field=IntegerField()
)
).annotate(
total_fee=F('fee') + F('deposit')
).annotate(
validate_status=Case(
When(total_fee__gt=F('user__note__balance'), then=Value(2)),
When(first_year=True, then=Value(1)),
default=Value(0),
output_field=IntegerField(),
)
)
buses = Bus.objects.filter(PermissionBackend.filter_queryset(self.request, Bus, "view")) \ buses = Bus.objects.filter(PermissionBackend.filter_queryset(self.request, Bus, "view")) \
.filter(wei=self.object).annotate(count=Count("memberships")).order_by("name") .filter(wei=self.object).annotate(count=Count("memberships")).order_by("name")
return [club_transactions, club_member, pre_registrations, buses, ] return [club_transactions, club_member, pre_registrations, buses, ]
@ -277,23 +258,6 @@ class WEIRegistrationsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTable
def get_queryset(self, **kwargs): def get_queryset(self, **kwargs):
qs = super().get_queryset(**kwargs).filter(wei=self.club, membership=None).distinct() qs = super().get_queryset(**kwargs).filter(wei=self.club, membership=None).distinct()
# Annotate the query to be able to sort registrations on validate status
qs = qs.annotate(
deposit=Case(
When(deposit_type='note', then=F('wei__deposit_amount')),
default=Value(0),
output_field=IntegerField()
)
).annotate(
total_fee=F('fee') + F('deposit')
).annotate(
validate_status=Case(
When(total_fee__gt=F('user__note__balance'), then=Value(2)),
When(first_year=True, then=Value(1)),
default=Value(0),
output_field=IntegerField(),
)
)
pattern = self.request.GET.get("search", "") pattern = self.request.GET.get("search", "")
@ -477,10 +441,6 @@ class BusTeamCreateView(ProtectQuerysetMixin, ProtectedCreateView):
self.object.refresh_from_db() self.object.refresh_from_db()
return reverse_lazy("wei:manage_bus_team", kwargs={"pk": self.object.pk}) return reverse_lazy("wei:manage_bus_team", kwargs={"pk": self.object.pk})
def get_template_names(self):
names = super().get_template_names()
return names
class BusTeamUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): class BusTeamUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
""" """
@ -513,10 +473,6 @@ class BusTeamUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
self.object.refresh_from_db() self.object.refresh_from_db()
return reverse_lazy("wei:manage_bus_team", kwargs={"pk": self.object.pk}) return reverse_lazy("wei:manage_bus_team", kwargs={"pk": self.object.pk})
def get_template_names(self):
names = super().get_template_names()
return names
class BusTeamManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): class BusTeamManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
""" """
@ -590,19 +546,9 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView):
def get_form(self, form_class=None): def get_form(self, form_class=None):
form = super().get_form(form_class) form = super().get_form(form_class)
form.fields["user"].initial = self.request.user form.fields["user"].initial = self.request.user
# Cacher les champs pendant l'inscription initiale
if "first_year" in form.fields:
del form.fields["first_year"] del form.fields["first_year"]
if "deposit_check" in form.fields: del form.fields["caution_check"]
del form.fields["deposit_check"]
if "information_json" in form.fields:
del form.fields["information_json"] del form.fields["information_json"]
if "deposit_type" in form.fields:
del form.fields["deposit_type"]
if "soge_credit" in form.fields:
form.fields["soge_credit"].help_text = _('Check if you will open a Société Générale account')
return form return form
@transaction.atomic @transaction.atomic
@ -694,27 +640,14 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
def get_form(self, form_class=None): def get_form(self, form_class=None):
form = super().get_form(form_class) form = super().get_form(form_class)
form.fields["user"].initial = self.request.user form.fields["user"].initial = self.request.user
if "soge_credit" in form.fields:
form.fields["soge_credit"].help_text = _('Check if you will open a Société Générale account')
if "myself" in self.request.path and self.request.user.profile.soge: if "myself" in self.request.path and self.request.user.profile.soge:
form.fields["soge_credit"].disabled = True form.fields["soge_credit"].disabled = True
form.fields["soge_credit"].help_text = _("You already opened an account in the Société générale.") form.fields["soge_credit"].help_text = _("You already opened an account in the Société générale.")
# Cacher les champs pendant l'inscription initiale del form.fields["caution_check"]
if "first_year" in form.fields:
del form.fields["first_year"] del form.fields["first_year"]
if "deposit_check" in form.fields:
del form.fields["deposit_check"]
if "information_json" in form.fields:
del form.fields["information_json"] del form.fields["information_json"]
# S'assurer que le champ deposit_type est obligatoire
if "deposit_type" in form.fields:
form.fields["deposit_type"].required = True
form.fields["deposit_type"].help_text = _("Choose how you want to pay the deposit")
form.fields["deposit_type"].widget = forms.RadioSelect(choices=form.fields["deposit_type"].choices)
return form return form
@transaction.atomic @transaction.atomic
@ -740,9 +673,6 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]] information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]]
information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]] information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]]
form.instance.information = information form.instance.information = information
# Sauvegarder le type de caution
form.instance.deposit_type = form.cleaned_data["deposit_type"]
form.instance.save() form.instance.save()
if 'treasury' in settings.INSTALLED_APPS: if 'treasury' in settings.INSTALLED_APPS:
@ -772,7 +702,6 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
# We can't update a registration once the WEI is started and before the membership start date # We can't update a registration once the WEI is started and before the membership start date
if today >= wei.date_start or today < wei.membership_start: if today >= wei.date_start or today < wei.membership_start:
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
# Store the validate parameter in the view's state
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -809,22 +738,12 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
form = super().get_form(form_class) form = super().get_form(form_class)
form.fields["user"].disabled = True form.fields["user"].disabled = True
# The auto-json-format may cause issues with the default field remove # The auto-json-format may cause issues with the default field remove
if "information_json" in form.fields: if not PermissionBackend.check_perm(self.request, 'wei.change_weiregistration_information_json', self.object):
del form.fields["information_json"] del form.fields["information_json"]
# Masquer le champ deposit_check pour tout le monde dans le formulaire de modification
if "deposit_check" in form.fields:
del form.fields["deposit_check"]
# S'assurer que le champ deposit_type est obligatoire pour les 2A+
if not self.object.first_year and "deposit_type" in form.fields:
form.fields["deposit_type"].required = True
form.fields["deposit_type"].help_text = _("Choose how you want to pay the deposit")
return form return form
def get_membership_form(self, data=None, instance=None): def get_membership_form(self, data=None, instance=None):
registration = self.get_object() membership_form = WEIMembershipForm(data if data else None, instance=instance)
membership_form = WEIMembershipForm(data if data else None, instance=instance, wei=registration.wei)
del membership_form.fields["credit_type"] del membership_form.fields["credit_type"]
del membership_form.fields["credit_amount"] del membership_form.fields["credit_amount"]
del membership_form.fields["first_name"] del membership_form.fields["first_name"]
@ -840,30 +759,10 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
def form_valid(self, form): def form_valid(self, form):
# If the membership is already validated, then we update the bus and the team (and the roles) # If the membership is already validated, then we update the bus and the team (and the roles)
if form.instance.is_validated: if form.instance.is_validated:
try: membership_form = self.get_membership_form(self.request.POST, form.instance.membership)
membership = form.instance.membership
if membership is None:
raise ValueError(_("No membership found for this registration"))
membership_form = self.get_membership_form(self.request.POST, instance=membership)
if not membership_form.is_valid(): if not membership_form.is_valid():
return self.form_invalid(form) return self.form_invalid(form)
# Vérifier que l'utilisateur a la permission de modifier le membership
# On vérifie d'abord si l'utilisateur a la permission générale de modification
if not self.request.user.has_perm("wei.change_weimembership"):
raise PermissionDenied(_("You don't have the permission to update memberships"))
# On vérifie ensuite les permissions spécifiques pour chaque champ modifié
for field_name in membership_form.changed_data:
perm = f"wei.change_weimembership_{field_name}"
if not self.request.user.has_perm(perm):
raise PermissionDenied(_("You don't have the permission to update the field %(field)s") % {'field': field_name})
membership_form.save() membership_form.save()
except (WEIMembership.DoesNotExist, ValueError, PermissionDenied) as e:
form.add_error(None, str(e))
return self.form_invalid(form)
# If it is not validated and if this is an old member, then we update the choices # If it is not validated and if this is an old member, then we update the choices
elif not form.instance.first_year and PermissionBackend.check_perm( elif not form.instance.first_year and PermissionBackend.check_perm(
self.request, "wei.change_weiregistration_information_json", self.object): self.request, "wei.change_weiregistration_information_json", self.object):
@ -878,10 +777,6 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]] information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]]
information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]] information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]]
form.instance.information = information form.instance.information = information
# Sauvegarder le type de caution pour les 2A+
if "deposit_type" in form.cleaned_data:
form.instance.deposit_type = form.cleaned_data["deposit_type"]
form.instance.save() form.instance.save()
return super().form_valid(form) return super().form_valid(form)
@ -892,6 +787,15 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
survey = CurrentSurvey(self.object) survey = CurrentSurvey(self.object)
if not survey.is_complete(): if not survey.is_complete():
return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk}) return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk})
if PermissionBackend.check_perm(self.request, "wei.add_weimembership", WEIMembership(
club=self.object.wei,
user=self.object.user,
date_start=date.today(),
date_end=date.today(),
fee=0,
registration=self.object,
)):
return reverse_lazy("wei:validate_registration", kwargs={"pk": self.object.pk})
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.wei.pk}) return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.wei.pk})
@ -932,23 +836,18 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
extra_context = {"title": _("Validate WEI registration")} extra_context = {"title": _("Validate WEI registration")}
def get_sample_object(self): def get_sample_object(self):
"""
Return a sample object for permission checking
"""
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"]) registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
return WEIMembership( return WEIMembership(
user=registration.user,
club=registration.wei, club=registration.wei,
date_start=registration.wei.date_start, user=registration.user,
fee=registration.wei.membership_fee_paid if registration.user.profile.paid else registration.wei.membership_fee_unpaid, date_start=date.today(),
# Add any fields needed for proper permission checking date_end=date.today() + timedelta(days=1),
fee=0,
registration=registration, registration=registration,
) )
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"]) wei = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei
wei = registration.wei
today = date.today() today = date.today()
# We can't validate anyone once the WEI is started and before the membership start date # We can't validate anyone once the WEI is started and before the membership start date
if today >= wei.date_start or today < wei.membership_start: if today >= wei.date_start or today < wei.membership_start:
@ -979,60 +878,39 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
date_start__gte=bde.membership_start, date_start__gte=bde.membership_start,
).exists() ).exists()
fee = registration.fee context["fee"] = registration.fee
context["fee"] = fee
# Calculer le montant total nécessaire (frais + caution si transaction)
total_needed = fee
if registration.deposit_type == 'note':
total_needed += registration.wei.deposit_amount
context["total_needed"] = total_needed
form = context["form"] form = context["form"]
if registration.soge_credit: if registration.soge_credit:
form.fields["credit_amount"].initial = fee form.fields["credit_amount"].initial = registration.fee
else: else:
form.fields["credit_amount"].initial = max(0, fee - registration.user.note.balance) form.fields["credit_amount"].initial = max(0, registration.fee - registration.user.note.balance)
return context return context
def get_form_class(self): def get_form_class(self):
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"]) registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
if registration.first_year and 'selected_bus_pk' not in registration.information: if registration.first_year and 'sleected_bus_pk' not in registration.information:
return WEIMembership1AForm return WEIMembership1AForm
return WEIMembershipForm return WEIMembershipForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
wei = registration.wei
kwargs['wei'] = wei
return kwargs
def get_form(self, form_class=None): def get_form(self, form_class=None):
form = super().get_form(form_class) form = super().get_form(form_class)
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"]) registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
form.fields["last_name"].initial = registration.user.last_name form.fields["last_name"].initial = registration.user.last_name
form.fields["first_name"].initial = registration.user.first_name form.fields["first_name"].initial = registration.user.first_name
# Ajouter le champ deposit_check uniquement pour les non-première année et le rendre obligatoire if "caution_check" in form.fields:
if not registration.first_year: form.fields["caution_check"].initial = registration.caution_check
if registration.deposit_type == 'check':
form.fields["deposit_check"] = forms.BooleanField( if registration.soge_credit:
required=True, form.fields["credit_type"].disabled = True
initial=registration.deposit_check, form.fields["credit_type"].initial = NoteSpecial.objects.get(special_type="Virement bancaire")
label=_("Deposit check given"), form.fields["credit_amount"].disabled = True
help_text=_("Please make sure the check is given before validating the registration") form.fields["last_name"].disabled = True
) form.fields["first_name"].disabled = True
else: form.fields["bank"].disabled = True
form.fields["deposit_check"] = forms.BooleanField( form.fields["bank"].initial = "Société générale"
required=True,
initial=False,
label=_("Create deposit transaction"),
help_text=_("A transaction of %(amount).2f€ will be created from the user's Note account") % {
'amount': registration.wei.deposit_amount / 100
}
)
if 'bus' in form.fields: if 'bus' in form.fields:
# For 2A+ and hardcoded 1A # For 2A+ and hardcoded 1A
@ -1066,8 +944,8 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
club = registration.wei club = registration.wei
user = registration.user user = registration.user
if "deposit_check" in form.data: if "caution_check" in form.data:
registration.deposit_check = form.data["deposit_check"] == "on" registration.caution_check = form.data["caution_check"] == "on"
registration.save() registration.save()
membership = form.instance membership = form.instance
membership.user = user membership.user = user
@ -1078,8 +956,6 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
membership._force_renew_parent = True membership._force_renew_parent = True
fee = club.membership_fee_paid if user.profile.paid else club.membership_fee_unpaid fee = club.membership_fee_paid if user.profile.paid else club.membership_fee_unpaid
if registration.soge_credit:
fee = registration.wei.fee_soge_credit
kfet = club.parent_club kfet = club.parent_club
bde = kfet.parent_club bde = kfet.parent_club
@ -1106,23 +982,13 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
first_name = form.cleaned_data["first_name"] first_name = form.cleaned_data["first_name"]
bank = form.cleaned_data["bank"] bank = form.cleaned_data["bank"]
if credit_type is None: if credit_type is None or registration.soge_credit:
credit_amount = 0 credit_amount = 0
# Calculer le montant total nécessaire (frais + caution si transaction) if not registration.soge_credit and user.note.balance + credit_amount < fee:
total_needed = fee # Users must have money before registering to the WEI.
if registration.deposit_type == 'note':
total_needed += club.deposit_amount
# Vérifier que l'utilisateur a assez d'argent pour tout payer
if user.note.balance + credit_amount < total_needed:
form.add_error('credit_type', form.add_error('credit_type',
_("This user doesn't have enough money to join this club and pay the deposit. " _("This user don't have enough money to join this club, and can't have a negative balance."))
"Current balance: %(balance)d€, credit: %(credit)d€, needed: %(needed)d") % {
'balance': user.note.balance,
'credit': credit_amount,
'needed': total_needed}
)
return super().form_invalid(form) return super().form_invalid(form)
if credit_amount: if credit_amount:
@ -1162,18 +1028,6 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
membership.refresh_from_db() membership.refresh_from_db()
membership.roles.add(WEIRole.objects.get(name="Adhérent⋅e WEI")) membership.roles.add(WEIRole.objects.get(name="Adhérent⋅e WEI"))
# Créer la transaction de caution si nécessaire
if registration.deposit_type == 'note':
from note.models import Transaction
Transaction.objects.create(
source=user.note,
destination=club.note,
quantity=1,
amount=club.deposit_amount,
reason=_("Deposit %(name)s") % {'name': club.name},
valid=True,
)
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self): def get_success_url(self):
@ -1393,7 +1247,6 @@ class WEI1AListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableView):
def get_queryset(self, filter_permissions=True, **kwargs): def get_queryset(self, filter_permissions=True, **kwargs):
qs = super().get_queryset(filter_permissions, **kwargs) qs = super().get_queryset(filter_permissions, **kwargs)
qs = qs.filter(first_year=True, membership__isnull=False) qs = qs.filter(first_year=True, membership__isnull=False)
qs = qs.filter(wei=self.club)
qs = qs.order_by('-membership__bus') qs = qs.order_by('-membership__bus')
return qs return qs
@ -1436,48 +1289,8 @@ class WEIAttributeBus1ANextView(LoginRequiredMixin, RedirectView):
if not wei.exists(): if not wei.exists():
raise Http404 raise Http404
wei = wei.get() wei = wei.get()
qs = WEIRegistration.objects.filter(wei=wei, membership__isnull=False, membership__bus__isnull=True)
# On cherche d'abord les 1A qui ont une inscription validée (membership) mais pas de bus qs = qs.filter(information_json__contains='selected_bus_pk') # not perfect, but works...
qs = WEIRegistration.objects.filter( if qs.exists():
wei=wei, return reverse_lazy('wei:wei_bus_1A', args=(qs.first().pk, ))
first_year=True, return reverse_lazy('wei:wei_1A_list', args=(wei.pk, ))
membership__isnull=False,
membership__bus__isnull=True
)
# Parmi eux, on prend ceux qui ont répondu au questionnaire (ont un bus préféré)
qs = qs.filter(information_json__contains='selected_bus_pk')
if not qs.exists():
# Si on ne trouve personne, on affiche un message et on retourne à la liste
messages.info(self.request, _("No first year student without a bus found. Either all of them have a bus, or none has filled the survey yet."))
return reverse_lazy('wei:wei_1A_list', args=(wei.pk,))
# On redirige vers la page d'attribution pour le premier étudiant trouvé
return reverse_lazy('wei:wei_bus_1A', args=(qs.first().pk,))
class BusInformationUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
model = Bus
def get_form_class(self):
return CurrentSurvey.get_algorithm_class().get_bus_information_form()
def dispatch(self, request, *args, **kwargs):
wei = self.get_object().wei
today = date.today()
# We can't update a bus once the WEI is started
if today >= wei.date_start:
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["club"] = self.object.wei
context["information"] = CurrentSurvey.get_algorithm_class().get_bus_information(self.object)
self.object.save()
return context
def get_success_url(self):
self.object.refresh_from_db()
return reverse_lazy("wei:manage_bus", kwargs={"pk": self.object.pk})

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -28,5 +28,4 @@ MAILTO=notekfet2020@lists.crans.org
00 6 * * * root cd /var/www/note_kfet && env/bin/python manage.py cleartokens -v 0 00 6 * * * root cd /var/www/note_kfet && env/bin/python manage.py cleartokens -v 0
# Envoyer la liste des abonnés à la NL BDA # Envoyer la liste des abonnés à la NL BDA
00 10 * * 0 root cd /var/www/note_kfet && env/bin/python manage.py extract_ml_registrations -t art -e "bda.ensparissaclay@gmail.com" 00 10 * * 0 root cd /var/www/note_kfet && env/bin/python manage.py extract_ml_registrations -t art -e "bda.ensparissaclay@gmail.com"
# Envoyer la liste de la bouffe au club et aux GCKs
00 8 * * 1 root cd /var/www/note_kfet && env/bin/python manage.py send_mail_for_food --report --club

View File

@ -56,8 +56,3 @@ if "cas_server" in settings.INSTALLED_APPS:
from cas_server.models import * from cas_server.models import *
admin_site.register(ServicePattern, ServicePatternAdmin) admin_site.register(ServicePattern, ServicePatternAdmin)
admin_site.register(FederatedIendityProvider, FederatedIendityProviderAdmin) admin_site.register(FederatedIendityProvider, FederatedIendityProviderAdmin)
if "constance" in settings.INSTALLED_APPS:
from constance.admin import *
from constance.models import *
admin_site.register([Config], ConstanceAdmin)

View File

@ -63,16 +63,8 @@ class ColorWidget(Widget):
def format_value(self, value): def format_value(self, value):
if value is None: if value is None:
value = 0xFFFFFF value = 0xFFFFFF
if isinstance(value, str):
return value # Assume it's already a hex string like "#FFAA33"
try:
return "#{:06X}".format(value) return "#{:06X}".format(value)
except Exception:
return "#FFFFFF"
def value_from_datadict(self, data, files, name): def value_from_datadict(self, data, files, name):
val = super().value_from_datadict(data, files, name) val = super().value_from_datadict(data, files, name)
if val:
return int(val[1:], 16) return int(val[1:], 16)
return None

View File

@ -39,9 +39,7 @@ SECURE_HSTS_PRELOAD = True
INSTALLED_APPS = [ INSTALLED_APPS = [
# External apps # External apps
'bootstrap_datepicker_plus', 'bootstrap_datepicker_plus',
'cas_server',
'colorfield', 'colorfield',
'constance',
'crispy_bootstrap4', 'crispy_bootstrap4',
'crispy_forms', 'crispy_forms',
# 'django_htcpcp_tea', # 'django_htcpcp_tea',
@ -113,7 +111,6 @@ TEMPLATES = [
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
'constance.context_processors.config',
'django.template.context_processors.debug', 'django.template.context_processors.debug',
'django.template.context_processors.request', 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
@ -273,7 +270,7 @@ OAUTH2_PROVIDER = {
'PKCE_REQUIRED': False, # PKCE (fix a breaking change of django-oauth-toolkit 2.0.0) 'PKCE_REQUIRED': False, # PKCE (fix a breaking change of django-oauth-toolkit 2.0.0)
'OIDC_ENABLED': True, 'OIDC_ENABLED': True,
'OIDC_RSA_PRIVATE_KEY': 'OIDC_RSA_PRIVATE_KEY':
os.getenv('OIDC_RSA_PRIVATE_KEY', 'CHANGE_ME_IN_ENV_SETTINGS').replace('\\n', '\n'), # for multilines os.getenv('OIDC_RSA_PRIVATE_KEY', '/var/secrets/oidc.key'),
'SCOPES': { 'openid': "OpenID Connect scope" }, 'SCOPES': { 'openid': "OpenID Connect scope" },
} }
@ -310,30 +307,6 @@ PHONENUMBER_DEFAULT_REGION = 'FR'
# We add custom information to CAS, in order to give a normalized name to other services # We add custom information to CAS, in order to give a normalized name to other services
CAS_AUTH_CLASS = 'member.auth.CustomAuthUser' CAS_AUTH_CLASS = 'member.auth.CustomAuthUser'
CAS_LOGIN_TEMPLATE = 'cas/login.html'
CAS_LOGOUT_TEMPLATE = 'cas/logout.html'
CAS_WARN_TEMPLATE = 'cas/warn.html'
CAS_LOGGED_TEMPLATE = 'cas/logged.html'
# Default field for primary key # Default field for primary key
DEFAULT_AUTO_FIELD = "django.db.models.AutoField" DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Constance settings
CONSTANCE_ADDITIONAL_FIELDS = {
'banner_type': ['django.forms.fields.ChoiceField', {
'widget': 'django.forms.Select',
'choices': (('info', 'Info'), ('success', 'Success'), ('warning', 'Warning'), ('danger', 'Danger'))
}],
}
CONSTANCE_CONFIG = {
'BANNER_MESSAGE': ('', 'Some message', str),
'BANNER_TYPE': ('info', 'Banner type', 'banner_type'),
'MAINTENANCE': (False, 'check for mainteance mode', bool),
'MAINTENANCE_MESSAGE': ('', 'Some maintenance message', str),
}
CONSTANCE_CONFIG_FIELDSETS = {
'Maintenance': ('MAINTENANCE_MESSAGE', 'MAINTENANCE'),
'Banner': ('BANNER_MESSAGE', 'BANNER_TYPE'),
}
CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'
CONSTANCE_SUPERUSER_ONLY = True

View File

@ -5,7 +5,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
<!DOCTYPE html> <!DOCTYPE html>
{% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %} {% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %}
<html lang="{{ LANGUAGE_CODE|default:"en" }}" {% if LANGUAGE_BIDI %}dir="rtl"{% endif %} class="position-relative h-100"> <html lang="{{ LANGUAGE_CODE|default:"en" }}" {% if LANGUAGE_BIDI %}dir="rtl"{% endif %} class="position-relative h-100">
{% if not config.MAINTENANCE %}
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
@ -139,12 +138,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a class="dropdown-item" href="{% url 'member:user_detail' pk=request.user.pk %}"> <a class="dropdown-item" href="{% url 'member:user_detail' pk=request.user.pk %}">
<i class="fa fa-user"></i> {% trans "My account" %} <i class="fa fa-user"></i> {% trans "My account" %}
</a> </a>
<form method="post" action="{% url 'logout' %}"> <a class="dropdown-item" href="{% url 'logout' %}">
{% csrf_token %}
<button class="dropdown-item" type=submit">
<i class="fa fa-sign-out"></i> {% trans "Log out" %} <i class="fa fa-sign-out"></i> {% trans "Log out" %}
</button> </a>
</form>
</div> </div>
</li> </li>
{% else %} {% else %}
@ -192,11 +188,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endblocktrans %} {% endblocktrans %}
</div> </div>
{% endif %} {% endif %}
{% if config.BANNER_MESSAGE and user.is_authenticated %} {# TODO Add banners #}
<div class="alert alert-{{ config.BANNER_TYPE }}">
{{ config.BANNER_MESSAGE }}
</div>
{% endif %}
</div> </div>
{% block content %} {% block content %}
<p>Default content...</p> <p>Default content...</p>
@ -218,10 +210,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
class="text-muted">{% trans "Charte Info (FR)" %}</a> &mdash; class="text-muted">{% trans "Charte Info (FR)" %}</a> &mdash;
<a href="https://note.crans.org/doc/faq/" <a href="https://note.crans.org/doc/faq/"
class="text-muted">{% trans "FAQ (FR)" %}</a> &mdash; class="text-muted">{% trans "FAQ (FR)" %}</a> &mdash;
<a href="https://bde.ens-cachan.fr"
class="text-muted">{% trans "Managed by BDE" %}</a> &mdash;
<a href="https://crans.org"
class="text-muted">{% trans "Hosted by Cr@ns" %}</a> &mdash;
</span> </span>
{% csrf_token %} {% csrf_token %}
<select title="language" name="language" <select title="language" name="language"
@ -258,15 +246,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% block extrajavascript %}{% endblock %} {% block extrajavascript %}{% endblock %}
</body> </body>
{% endif %}
{% if config.MAINTENANCE %}
<body>
<div style="text-align:center">
<br />
{% trans "The note is not available for now" %}<br /><br />
{{ config.MAINTENANCE_MESSAGE }}<br /><br />
{% trans "Thank you for your understanding -- The Respos Info of BDE" %}
</div>
</body>
{% endif %}
</html> </html>

View File

@ -1,28 +0,0 @@
{% extends "base.html" %}
{% comment %}
Copyright (C) by BDE ENS-Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% block content %}
<div class="alert alert-success" role="alert">{% blocktrans %}<h3>Log In Successful</h3>You have successfully logged into the Central Authentication Service.<br/>For security reasons, please Log Out and Exit your web browser when you are done accessing services that require authentication!{% endblocktrans %}</div>
<div class="card bg-light mx-auto" style="max-width:30rem;">
<div class="card-body">
<form class="form-signin" method="get" action="logout">
<div class="checkbox">
<label>
<input type="checkbox" name="all" value="1">{% trans "Log me out from all my sessions" %}
</label>
</div>
{% if settings.CAS_FEDERATE and request.COOKIES.remember_provider %}
<div class="checkbox">
<label>
<input type="checkbox" name="forget_provider" value="1">{% trans "Forget the identity provider" %}
</label>
</div>
{% endif %}
<button class="btn btn-danger btn-block btn-lg" type="submit">{% trans "Logout" %}</button>
</form>
</div>
</div>
{% endblock %}

View File

@ -1,42 +0,0 @@
{% extends "base.html" %}
{% comment %}
Copyright (C) by BDE ENS-Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% block ante_messages %}
{% if auto_submit %}<noscript>{% endif %}
<div class="card-header text-center">
<h2 class="form-signin-heading">{% trans "Please log in" %}</h2>
</div>
{% if auto_submit %}</noscript>{% endif %}
{% endblock %}
{% block content %}
<div class="card bg-light mx-auto" style="max-width: 30rem;">
<div class="card-body">
<form class="form-signin" method="post" id="login_form"{% if post_url %} action="{{post_url}}"{% endif %}>
{% csrf_token %}
{% include "cas_server/bs4/form.html" %}
{% if auto_submit %}<noscript>{% endif %}
<button class="btn btn-primary btn-block btn-lg" type="submit">{% trans "Login" %}</button>
{% if auto_submit %}</noscript>{% endif %}
</div>
</form>
</div>
</div>
{% endblock %}
{% block javascript_inline %}
jQuery(function( $ ){
$("#id_warn").click(function(e){
if($("#id_warn").is(':checked')){
createCookie("warn", "on", 10 * 365);
} else {
eraseCookie("warn");
}
});
});
{% if auto_submit %}document.getElementById('login_form').submit(); // SUBMIT FORM{% endif %}
{% endblock %}

View File

@ -1,10 +0,0 @@
{% extends "base.html" %}
{% comment %}
Copyright (C) by BDE ENS-Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n static %}
{% block content %}
<div class="alert alert-success" role="alert">{{ logout_msg }}</div>
{% endblock %}

View File

@ -1,19 +0,0 @@
{% extends "base.html" %}
{% comment %}
Copyright (C) by BDE ENS-Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n static %}
{% block content %}
<div class="card bg-light mx-auto" style="max-width: 30rem;">
<div class="card-body">
<form class="form-signin" method="post">
{% csrf_token %}
{% include "cas_server/bs4/form.html" %}
<button class="btn btn-primary btn-block btn-lg" type="submit">{% trans "Connect to the service" %}</button>
</form>
</div>
</div>
{% endblock %}

View File

@ -1,5 +0,0 @@
<input type="text"
name="{{ widget.name }}"
value="{{ widget.value }}"
class="jscolor"
{% include "django/forms/widgets/attrs.html" %}>

View File

@ -1,21 +1,20 @@
beautifulsoup4~=4.13.4 beautifulsoup4~=4.12.3
crispy-bootstrap4~=2025.6 crispy-bootstrap4~=2023.1
Django~=5.2.4 Django~=4.2.9
django-bootstrap-datepicker-plus~=5.0.5 django-bootstrap-datepicker-plus~=5.0.5
django-cas-server~=3.1.0 #django-cas-server~=2.0.0
django-colorfield~=0.14.0 django-colorfield~=0.11.0
django-constance~=4.3.2 django-crispy-forms~=2.1.0
django-crispy-forms~=2.4.0 django-extensions>=3.2.3
django-extensions>=4.1.0 django-filter~=23.5
django-filter~=25.1
#django-htcpcp-tea~=0.8.1 #django-htcpcp-tea~=0.8.1
django-mailer~=2.3.2 django-mailer~=2.3.1
django-oauth-toolkit~=3.0.1 django-oauth-toolkit~=2.3.0
django-phonenumber-field~=8.1.0 django-phonenumber-field~=7.3.0
django-polymorphic~=3.1.0 django-polymorphic~=3.1.0
djangorestframework~=3.16.0 djangorestframework~=3.14.0
django-rest-polymorphic~=0.1.10 django-rest-polymorphic~=0.1.10
django-tables2~=2.7.5 django-tables2~=2.7.0
python-memcached~=1.62 python-memcached~=1.62
phonenumbers~=9.0.8 phonenumbers~=8.13.28
Pillow>=11.3.0 Pillow>=10.2.0

View File

@ -1,13 +1,13 @@
[tox] [tox]
envlist = envlist =
# Ubuntu 22.04 Python # Ubuntu 22.04 Python
py310-django52 py310-django42
# Debian Bookworm Python # Debian Bookworm Python
py311-django52 py311-django42
# Ubuntu 24.04 Python # Ubuntu 24.04 Python
py312-django52 py312-django42
linters linters
skipsdist = True skipsdist = True
@ -32,7 +32,8 @@ deps =
pep8-naming pep8-naming
pyflakes pyflakes
commands = commands =
flake8 apps --extend-exclude apps/scripts flake8 apps --extend-exclude apps/scripts,apps/wrapped/management/commands
flake8 apps/wrapped/management/commands --extend-ignore=C901
[flake8] [flake8]
ignore = W503, I100, I101, B019 ignore = W503, I100, I101, B019