1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-07-20 08:01:26 +02:00

Compare commits

...

23 Commits

Author SHA1 Message Date
b97b79e2ea translation 2025-07-12 14:05:53 +02:00
695ce63e08 Merge branch 'food_traceability' into 'beta'
Easier access to food details

See merge request bde/nk20!326
2025-07-11 17:15:50 +02:00
79f50c27f1 Merge branch 'beta' into 'food_traceability'
# Conflicts:
#   locale/fr/LC_MESSAGES/django.po
2025-07-11 17:00:45 +02:00
5989721bc9 Easier access to food details 2025-07-11 16:35:49 +02:00
bcc3e7cc53 Merge branch 'food_traceability' into beta 2025-07-11 12:26:55 +02:00
608804db30 Bugs fixed 2025-07-10 20:05:27 +02:00
82a06c29dd linters 2025-07-09 16:12:55 +02:00
cf9d208586 scopes 2025-07-09 15:57:24 +02:00
432f50e49a propose fix for #134 (partially tested) 2025-07-09 00:15:33 +02:00
883589e08c django-constance and traduction 2025-07-06 16:17:13 +02:00
c36f8c25a2 Add banner #80 (with django-constance 2025-07-05 18:45:36 +02:00
8783a63d7f change CAS template for #133 2025-07-05 13:56:43 +02:00
4cc43fe4b6 traduction, resolve #133 2025-07-04 22:11:47 +02:00
b7c0986a5f cron and linters 2025-07-04 17:14:12 +02:00
85ea43a7cf change pipeline 2025-07-04 16:27:04 +02:00
f54dd30482 fix logout test 2025-07-03 15:18:29 +02:00
7eafe33945 Merge branch 'main' into django-5.2 2025-07-03 14:24:58 +02:00
6edef619aa change requirements.txt 2025-07-03 11:37:07 +02:00
8a1f30ebe2 Merge branch 'wei' into 'main'
Wei

See merge request bde/nk20!325
2025-07-01 18:14:45 +02:00
b2c6b0e85d Sélection de bus/équipe plus ergonomique 2025-07-01 17:48:39 +02:00
bc517f02e5 Traduction 2025-06-27 19:26:58 +02:00
e83ee8015f Tests 2025-06-27 18:50:37 +02:00
c26534b6b7 Année et algorithme 2025-06-27 16:56:17 +02:00
31 changed files with 1078 additions and 194 deletions

View File

@ -8,7 +8,7 @@ variables:
GIT_SUBMODULE_STRATEGY: recursive GIT_SUBMODULE_STRATEGY: recursive
# Ubuntu 22.04 # Ubuntu 22.04
py310-django42: py310-django52:
stage: test stage: test
image: ubuntu:22.04 image: ubuntu:22.04
before_script: before_script:
@ -22,10 +22,10 @@ py310-django42:
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-django42 script: tox -e py310-django52
# Debian Bookworm # Debian Bookworm
py311-django42: py311-django52:
stage: test stage: test
image: debian:bookworm image: debian:bookworm
before_script: before_script:
@ -37,7 +37,7 @@ py311-django42:
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-django42 script: tox -e py311-django52
linters: linters:
stage: quality-assurance stage: quality-assurance

View File

@ -7,7 +7,52 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% load i18n %} {% load i18n %}
{% block content %} {% block content %}
{{ block.super }} <div class="card bg-light">
<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">
@ -68,4 +113,20 @@ 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,4 +18,5 @@ 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,6 +10,7 @@ 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 _
@ -507,3 +508,14 @@ 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

@ -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.get(reverse("logout")) response = self.client.post(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

@ -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) router.register(path + '/consumer', ConsumerViewSet, basename='alias2')
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

@ -18,7 +18,18 @@ 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): def get_all_scopes(self, **kwargs):
scopes = {}
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})" 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" scopes['openid'] = "OpenID Connect"

View File

@ -13,6 +13,7 @@ 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',

View File

@ -164,14 +164,24 @@ class ScopesView(LoginRequiredMixin, TemplateView):
from oauth2_provider.models import Application from oauth2_provider.models import Application
from .scopes import PermissionScopes from .scopes import PermissionScopes
scopes = PermissionScopes() oidc = False
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 = scopes.get_available_scopes(app) available_scopes = PermissionScopes().get_available_scopes(app)
context["scopes"][app] = OrderedDict() context["scopes"][app] = OrderedDict()
items = [(k, v) for (k, v) in all_scopes.items() if k in available_scopes] all_scopes = PermissionScopes().get_all_scopes(scopes=available_scopes)
# items.sort(key=lambda x: (int(x[0].split("_")[1]), int(x[0].split("_")[0]))) 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])))
# 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

@ -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 from django.forms import CheckboxSelectMultiple, RadioSelect
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
@ -140,6 +140,19 @@ 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 \
@ -151,21 +164,8 @@ class WEIMembershipForm(forms.ModelForm):
model = WEIMembership model = WEIMembership
fields = ('roles', 'bus', 'team',) fields = ('roles', 'bus', 'team',)
widgets = { widgets = {
"bus": Autocomplete( "bus": RadioSelect(),
Bus, "team": RadioSelect(),
attrs={
'api_url': '/api/wei/bus/',
'placeholder': 'Bus ...',
}
),
"team": Autocomplete(
BusTeam,
attrs={
'api_url': '/api/wei/team/',
'placeholder': 'Équipe ...',
},
resetable=True,
),
} }
@ -213,4 +213,3 @@ class BusTeamForm(forms.ModelForm):
), ),
"color": ColorWidget(), "color": ColorWidget(),
} }
# "color": ColorWidget(),

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 .wei2024 import WEISurvey2024 from .wei2025 import WEISurvey2025
__all__ = [ __all__ = [
'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey', 'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey',
] ]
CurrentSurvey = WEISurvey2024 CurrentSurvey = WEISurvey2025

View File

@ -121,6 +121,13 @@ 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

@ -0,0 +1,347 @@
# 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

@ -22,6 +22,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endif %} {% 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

@ -210,4 +210,27 @@ 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,8 +6,6 @@ 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
@ -129,44 +127,3 @@ 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

@ -0,0 +1,111 @@
# 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

@ -778,7 +778,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(), 2024) self.assertEqual(CurrentSurvey.get_year(), 2025)
class TestWeiAPI(TestAPI): class TestWeiAPI(TestAPI):

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, \ WEIRegistrationsView, WEIMembershipsView, MemberListRenderView, BusInformationUpdateView, \
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,4 +42,5 @@ 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

@ -788,7 +788,8 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
return form return form
def get_membership_form(self, data=None, instance=None): def get_membership_form(self, data=None, instance=None):
membership_form = WEIMembershipForm(data if data else None, instance=instance) registration = self.get_object()
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"]
@ -969,6 +970,13 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
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"])
@ -1422,3 +1430,29 @@ class WEIAttributeBus1ANextView(LoginRequiredMixin, RedirectView):
# On redirige vers la page d'attribution pour le premier étudiant trouvé # On redirige vers la page d'attribution pour le premier étudiant trouvé
return reverse_lazy('wei:wei_bus_1A', args=(qs.first().pk,)) 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})

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-20 13:50+0200\n" "POT-Creation-Date: 2025-07-11 16:10+0200\n"
"PO-Revision-Date: 2022-04-11 22:05+0200\n" "PO-Revision-Date: 2022-04-11 22:05+0200\n"
"Last-Translator: bleizi <bleizi@crans.org>\n" "Last-Translator: bleizi <bleizi@crans.org>\n"
"Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\n" "Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\n"
@ -357,7 +357,7 @@ msgstr "Détails de l'activité"
#: apps/note/models/transactions.py:261 #: apps/note/models/transactions.py:261
#: apps/note/templates/note/transaction_form.html:17 #: apps/note/templates/note/transaction_form.html:17
#: apps/note/templates/note/transaction_form.html:152 #: apps/note/templates/note/transaction_form.html:152
#: note_kfet/templates/base.html:78 #: note_kfet/templates/base.html:79
msgid "Transfer" msgid "Transfer"
msgstr "Virement" msgstr "Virement"
@ -474,7 +474,7 @@ msgstr "Inviter"
msgid "Create new activity" msgid "Create new activity"
msgstr "Créer une nouvelle activité" msgstr "Créer une nouvelle activité"
#: apps/activity/views.py:71 note_kfet/templates/base.html:96 #: apps/activity/views.py:71 note_kfet/templates/base.html:97
msgid "Activities" msgid "Activities"
msgstr "Activités" msgstr "Activités"
@ -563,7 +563,7 @@ msgstr "Nom"
#, fuzzy #, fuzzy
#| msgid "QR-code number" #| msgid "QR-code number"
msgid "QR code number" msgid "QR code number"
msgstr "numéro de QR-code" msgstr "Numéro de QR-code"
#: apps/food/models.py:23 #: apps/food/models.py:23
msgid "Allergen" msgid "Allergen"
@ -597,7 +597,7 @@ msgstr "est prêt"
msgid "order" msgid "order"
msgstr "consigne" msgstr "consigne"
#: apps/food/models.py:107 apps/food/views.py:34 #: apps/food/models.py:107 apps/food/views.py:35
#: note_kfet/templates/base.html:72 #: note_kfet/templates/base.html:72
msgid "Food" msgid "Food"
msgstr "Bouffe" msgstr "Bouffe"
@ -657,61 +657,75 @@ msgstr "QR-codes"
#: apps/food/models.py:286 #: apps/food/models.py:286
#: apps/food/templates/food/transformedfood_update.html:24 #: apps/food/templates/food/transformedfood_update.html:24
msgid "QR-code number" msgid "QR-code number"
msgstr "numéro de QR-code" msgstr "Numéro de QR-code"
#: apps/food/templates/food/food_detail.html:19 #: apps/food/templates/food/food_detail.html:22
msgid "Contained in" msgid "Contained in"
msgstr "Contenu dans" msgstr "Contenu dans"
#: apps/food/templates/food/food_detail.html:26 #: apps/food/templates/food/food_detail.html:29
msgid "Contain" msgid "Contain"
msgstr "Contient" msgstr "Contient"
#: apps/food/templates/food/food_detail.html:35 #: apps/food/templates/food/food_detail.html:38
msgid "Update" msgid "Update"
msgstr "Modifier" msgstr "Modifier"
#: apps/food/templates/food/food_detail.html:40 #: apps/food/templates/food/food_detail.html:43
msgid "Add to a meal" msgid "Add to a meal"
msgstr "Ajouter à un plat" msgstr "Ajouter à un plat"
#: apps/food/templates/food/food_detail.html:45 #: apps/food/templates/food/food_detail.html:48
msgid "Manage ingredients" msgid "Manage ingredients"
msgstr "Gérer les ingrédients" msgstr "Gérer les ingrédients"
#: apps/food/templates/food/food_detail.html:49 #: apps/food/templates/food/food_detail.html:52
msgid "Return to the food list" msgid "Return to the food list"
msgstr "Retour à la liste de nourriture" msgstr "Retour à la liste de nourriture"
#: apps/food/templates/food/food_list.html:14 #: apps/food/templates/food/food_list.html:32
msgid "View food"
msgstr "Voir l'aliment"
#: apps/food/templates/food/food_list.html:37
#: note_kfet/templates/base_search.html:15
msgid "Search by attribute such as name..."
msgstr "Chercher par un attribut tel que le nom..."
#: apps/food/templates/food/food_list.html:49
#: note_kfet/templates/base_search.html:23
msgid "There is no results."
msgstr "Il n'y a pas de résultat."
#: apps/food/templates/food/food_list.html:58
msgid "Meal served" msgid "Meal served"
msgstr "Plat servis" msgstr "Plat servis"
#: apps/food/templates/food/food_list.html:19 #: apps/food/templates/food/food_list.html:63
msgid "New meal" msgid "New meal"
msgstr "Nouveau plat" msgstr "Nouveau plat"
#: apps/food/templates/food/food_list.html:28 #: apps/food/templates/food/food_list.html:72
msgid "There is no meal served." msgid "There is no meal served."
msgstr "Il n'y a pas de plat servi." msgstr "Il n'y a pas de plat servi."
#: apps/food/templates/food/food_list.html:35 #: apps/food/templates/food/food_list.html:79
msgid "Free food" msgid "Free food"
msgstr "Open" msgstr "Open"
#: apps/food/templates/food/food_list.html:42 #: apps/food/templates/food/food_list.html:86
msgid "There is no free food." msgid "There is no free food."
msgstr "Il n'y a pas de bouffe en open" msgstr "Il n'y a pas de bouffe en open"
#: apps/food/templates/food/food_list.html:50 #: apps/food/templates/food/food_list.html:94
msgid "Food of your clubs" msgid "Food of your clubs"
msgstr "Bouffe de tes clubs" msgstr "Bouffe de tes clubs"
#: apps/food/templates/food/food_list.html:56 #: apps/food/templates/food/food_list.html:100
msgid "Food of club" msgid "Food of club"
msgstr "Bouffe du club" msgstr "Bouffe du club"
#: apps/food/templates/food/food_list.html:63 #: apps/food/templates/food/food_list.html:107
msgid "Yours club has not food yet." msgid "Yours club has not food yet."
msgstr "Ton club n'a pas de bouffe pour l'instant" msgstr "Ton club n'a pas de bouffe pour l'instant"
@ -785,49 +799,49 @@ msgstr "semaines"
msgid "and" msgid "and"
msgstr "et" msgstr "et"
#: apps/food/views.py:118 #: apps/food/views.py:120
msgid "Add a new QRCode" msgid "Add a new QRCode"
msgstr "Ajouter un nouveau QR-code" msgstr "Ajouter un nouveau QR-code"
#: apps/food/views.py:167 #: apps/food/views.py:169
msgid "Add an aliment" msgid "Add an aliment"
msgstr "Ajouter un nouvel aliment" msgstr "Ajouter un nouvel aliment"
#: apps/food/views.py:235 #: apps/food/views.py:228
msgid "Add a meal" msgid "Add a meal"
msgstr "Ajouter un plat" msgstr "Ajouter un plat"
#: apps/food/views.py:275 #: apps/food/views.py:259
msgid "Manage ingredients of:" msgid "Manage ingredients of:"
msgstr "Gestion des ingrédienrs de :" msgstr "Gestion des ingrédienrs de :"
#: apps/food/views.py:289 apps/food/views.py:297 #: apps/food/views.py:273 apps/food/views.py:281
#, python-brace-format #, python-brace-format
msgid "Fully used in {meal}" msgid "Fully used in {meal}"
msgstr "Aliment entièrement utilisé dans : {meal}" msgstr "Aliment entièrement utilisé dans : {meal}"
#: apps/food/views.py:344 #: apps/food/views.py:320
msgid "Add the ingredient:" msgid "Add the ingredient:"
msgstr "Ajouter l'ingrédient" msgstr "Ajouter l'ingrédient"
#: apps/food/views.py:370 #: apps/food/views.py:346
#, python-brace-format #, python-brace-format
msgid "Food fully used in : {meal.name}" msgid "Food fully used in : {meal.name}"
msgstr "Aliment entièrement utilisé dans : {meal.name}" msgstr "Aliment entièrement utilisé dans : {meal.name}"
#: apps/food/views.py:389 #: apps/food/views.py:365
msgid "Update an aliment" msgid "Update an aliment"
msgstr "Modifier un aliment" msgstr "Modifier un aliment"
#: apps/food/views.py:437 #: apps/food/views.py:413
msgid "Details of:" msgid "Details of:"
msgstr "Détails de :" msgstr "Détails de :"
#: apps/food/views.py:447 apps/treasury/tables.py:149 #: apps/food/views.py:423 apps/treasury/tables.py:149
msgid "Yes" msgid "Yes"
msgstr "Oui" msgstr "Oui"
#: apps/food/views.py:449 apps/member/models.py:99 apps/treasury/tables.py:149 #: apps/food/views.py:425 apps/member/models.py:99 apps/treasury/tables.py:149
msgid "No" msgid "No"
msgstr "Non" msgstr "Non"
@ -1962,8 +1976,8 @@ msgstr ""
"mode de paiement et un⋅e utilisateur⋅rice ou un club" "mode de paiement et un⋅e utilisateur⋅rice ou un club"
#: apps/note/models/transactions.py:357 apps/note/models/transactions.py:360 #: apps/note/models/transactions.py:357 apps/note/models/transactions.py:360
#: apps/note/models/transactions.py:363 apps/wei/views.py:1097 #: apps/note/models/transactions.py:363 apps/wei/views.py:1105
#: apps/wei/views.py:1101 #: apps/wei/views.py:1109
msgid "This field is required." msgid "This field is required."
msgstr "Ce champ est requis." msgstr "Ce champ est requis."
@ -2065,6 +2079,8 @@ msgstr "Historique des transactions récentes"
#: apps/note/templates/note/mails/weekly_report.txt:32 #: apps/note/templates/note/mails/weekly_report.txt:32
#: apps/registration/templates/registration/mails/email_validation_email.html:40 #: apps/registration/templates/registration/mails/email_validation_email.html:40
#: apps/registration/templates/registration/mails/email_validation_email.txt:16 #: apps/registration/templates/registration/mails/email_validation_email.txt:16
#: apps/scripts/templates/scripts/food_report.html:48
#: apps/scripts/templates/scripts/food_report.txt:14
msgid "Mail generated by the Note Kfet on the" msgid "Mail generated by the Note Kfet on the"
msgstr "Mail généré par la Note Kfet le" msgstr "Mail généré par la Note Kfet le"
@ -2176,7 +2192,7 @@ msgstr "Chercher un bouton"
msgid "Update button" msgid "Update button"
msgstr "Modifier le bouton" msgstr "Modifier le bouton"
#: apps/note/views.py:156 note_kfet/templates/base.html:66 #: apps/note/views.py:156 note_kfet/templates/base.html:67
msgid "Consumptions" msgid "Consumptions"
msgstr "Consommations" msgstr "Consommations"
@ -2269,7 +2285,7 @@ msgstr "s'applique au club"
msgid "role permissions" msgid "role permissions"
msgstr "permissions par rôles" msgstr "permissions par rôles"
#: apps/permission/signals.py:73 #: apps/permission/signals.py:75
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"You don't have the permission to change the field {field} on this instance " "You don't have the permission to change the field {field} on this instance "
@ -2278,7 +2294,7 @@ msgstr ""
"Vous n'avez pas la permission de modifier le champ {field} sur l'instance du " "Vous n'avez pas la permission de modifier le champ {field} sur l'instance du "
"modèle {app_label}.{model_name}." "modèle {app_label}.{model_name}."
#: apps/permission/signals.py:83 apps/permission/views.py:104 #: apps/permission/signals.py:85 apps/permission/views.py:104
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"You don't have the permission to add an instance of model {app_label}." "You don't have the permission to add an instance of model {app_label}."
@ -2287,7 +2303,7 @@ msgstr ""
"Vous n'avez pas la permission d'ajouter une instance du modèle {app_label}." "Vous n'avez pas la permission d'ajouter une instance du modèle {app_label}."
"{model_name}." "{model_name}."
#: apps/permission/signals.py:112 #: apps/permission/signals.py:114
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"You don't have the permission to delete this instance of model {app_label}." "You don't have the permission to delete this instance of model {app_label}."
@ -2375,7 +2391,7 @@ msgstr ""
"Vous n'avez pas la permission d'ajouter une instance du modèle « {model} » " "Vous n'avez pas la permission d'ajouter une instance du modèle « {model} » "
"avec ces paramètres. Merci de les corriger et de réessayer." "avec ces paramètres. Merci de les corriger et de réessayer."
#: apps/permission/views.py:111 note_kfet/templates/base.html:120 #: apps/permission/views.py:111 note_kfet/templates/base.html:121
msgid "Rights" msgid "Rights"
msgstr "Droits" msgstr "Droits"
@ -2580,7 +2596,7 @@ msgstr ""
msgid "Invalidate pre-registration" msgid "Invalidate pre-registration"
msgstr "Invalider l'inscription" msgstr "Invalider l'inscription"
#: apps/treasury/apps.py:12 note_kfet/templates/base.html:102 #: apps/treasury/apps.py:12 note_kfet/templates/base.html:103
msgid "Treasury" msgid "Treasury"
msgstr "Trésorerie" msgstr "Trésorerie"
@ -2996,7 +3012,7 @@ msgstr "Gérer les crédits de la Société générale"
#: apps/wei/apps.py:10 apps/wei/models.py:42 apps/wei/models.py:43 #: apps/wei/apps.py:10 apps/wei/models.py:42 apps/wei/models.py:43
#: apps/wei/models.py:67 apps/wei/models.py:192 #: apps/wei/models.py:67 apps/wei/models.py:192
#: note_kfet/templates/base.html:108 #: note_kfet/templates/base.html:109
msgid "WEI" msgid "WEI"
msgstr "WEI" msgstr "WEI"
@ -3041,14 +3057,19 @@ msgstr "Rôles au WEI"
msgid "Select the roles that you are interested in." msgid "Select the roles that you are interested in."
msgstr "Sélectionnez les rôles qui vous intéressent." msgstr "Sélectionnez les rôles qui vous intéressent."
#: apps/wei/forms/registration.py:147 #: apps/wei/forms/registration.py:160
msgid "This team doesn't belong to the given bus." msgid "This team doesn't belong to the given bus."
msgstr "Cette équipe n'appartient pas à ce bus." msgstr "Cette équipe n'appartient pas à ce bus."
#: apps/wei/forms/surveys/wei2021.py:35 apps/wei/forms/surveys/wei2022.py:38 #: apps/wei/forms/surveys/wei2021.py:35 apps/wei/forms/surveys/wei2022.py:38
#: apps/wei/forms/surveys/wei2025.py:36
msgid "Choose a word:" msgid "Choose a word:"
msgstr "Choisissez un mot :" msgstr "Choisissez un mot :"
#: apps/wei/forms/surveys/wei2025.py:123
msgid "Rate between 0 and 5."
msgstr "Note entre 0 et 5."
#: apps/wei/models.py:25 apps/wei/templates/wei/base.html:36 #: apps/wei/models.py:25 apps/wei/templates/wei/base.html:36
msgid "year" msgid "year"
msgstr "année" msgstr "année"
@ -3115,7 +3136,7 @@ msgstr "Rôle au WEI"
msgid "Credit from Société générale" msgid "Credit from Société générale"
msgstr "Crédit de la Société générale" msgstr "Crédit de la Société générale"
#: apps/wei/models.py:202 apps/wei/views.py:984 #: apps/wei/models.py:202 apps/wei/views.py:992
msgid "Caution check given" msgid "Caution check given"
msgstr "Chèque de caution donné" msgstr "Chèque de caution donné"
@ -3250,7 +3271,7 @@ msgstr "Année"
msgid "preferred bus" msgid "preferred bus"
msgstr "bus préféré" msgstr "bus préféré"
#: apps/wei/tables.py:210 apps/wei/templates/wei/bus_detail.html:36 #: apps/wei/tables.py:210 apps/wei/templates/wei/bus_detail.html:38
#: apps/wei/templates/wei/busteam_detail.html:52 #: apps/wei/templates/wei/busteam_detail.html:52
msgid "Teams" msgid "Teams"
msgstr "Équipes" msgstr "Équipes"
@ -3344,18 +3365,22 @@ msgstr "Voir le WEI"
#: apps/wei/templates/wei/bus_detail.html:21 #: apps/wei/templates/wei/bus_detail.html:21
msgid "View club" msgid "View club"
msgstr "Voir le lub" msgstr "Voir le club"
#: apps/wei/templates/wei/bus_detail.html:26 #: apps/wei/templates/wei/bus_detail.html:26
msgid "Edit information"
msgstr "Modifier les informations"
#: apps/wei/templates/wei/bus_detail.html:28
#: apps/wei/templates/wei/busteam_detail.html:24 #: apps/wei/templates/wei/busteam_detail.html:24
msgid "Add team" msgid "Add team"
msgstr "Ajouter une équipe" msgstr "Ajouter une équipe"
#: apps/wei/templates/wei/bus_detail.html:49 #: apps/wei/templates/wei/bus_detail.html:51
msgid "Members" msgid "Members"
msgstr "Membres" msgstr "Membres"
#: apps/wei/templates/wei/bus_detail.html:58 #: apps/wei/templates/wei/bus_detail.html:60
#: apps/wei/templates/wei/busteam_detail.html:62 #: apps/wei/templates/wei/busteam_detail.html:62
#: apps/wei/templates/wei/weimembership_list.html:31 #: apps/wei/templates/wei/weimembership_list.html:31
msgid "View as PDF" msgid "View as PDF"
@ -3363,8 +3388,8 @@ msgstr "Télécharger au format PDF"
#: apps/wei/templates/wei/survey.html:11 #: apps/wei/templates/wei/survey.html:11
#: apps/wei/templates/wei/survey_closed.html:11 #: apps/wei/templates/wei/survey_closed.html:11
#: apps/wei/templates/wei/survey_end.html:11 apps/wei/views.py:1159 #: apps/wei/templates/wei/survey_end.html:11 apps/wei/views.py:1167
#: apps/wei/views.py:1214 apps/wei/views.py:1261 #: apps/wei/views.py:1222 apps/wei/views.py:1269
msgid "Survey WEI" msgid "Survey WEI"
msgstr "Questionnaire WEI" msgstr "Questionnaire WEI"
@ -3644,51 +3669,51 @@ msgstr ""
msgid "Update WEI Registration" msgid "Update WEI Registration"
msgstr "Modifier l'inscription WEI" msgstr "Modifier l'inscription WEI"
#: apps/wei/views.py:810 #: apps/wei/views.py:811
msgid "No membership found for this registration" msgid "No membership found for this registration"
msgstr "Pas d'adhésion trouvée pour cette inscription" msgstr "Pas d'adhésion trouvée pour cette inscription"
#: apps/wei/views.py:819 #: apps/wei/views.py:820
msgid "You don't have the permission to update memberships" msgid "You don't have the permission to update memberships"
msgstr "" msgstr ""
"Vous n'avez pas la permission d'ajouter une instance du modèle {app_label}." "Vous n'avez pas la permission d'ajouter une instance du modèle {app_label}."
"{model_name}." "{model_name}."
#: apps/wei/views.py:825 #: apps/wei/views.py:826
#, python-format #, python-format
msgid "You don't have the permission to update the field %(field)s" msgid "You don't have the permission to update the field %(field)s"
msgstr "Vous n'avez pas la permission de modifier le champ %(field)s" msgstr "Vous n'avez pas la permission de modifier le champ %(field)s"
#: apps/wei/views.py:870 #: apps/wei/views.py:871
msgid "Delete WEI registration" msgid "Delete WEI registration"
msgstr "Supprimer l'inscription WEI" msgstr "Supprimer l'inscription WEI"
#: apps/wei/views.py:881 #: apps/wei/views.py:882
msgid "You don't have the right to delete this WEI registration." msgid "You don't have the right to delete this WEI registration."
msgstr "Vous n'avez pas la permission de supprimer cette inscription au WEI." msgstr "Vous n'avez pas la permission de supprimer cette inscription au WEI."
#: apps/wei/views.py:899 #: apps/wei/views.py:900
msgid "Validate WEI registration" msgid "Validate WEI registration"
msgstr "Valider l'inscription WEI" msgstr "Valider l'inscription WEI"
#: apps/wei/views.py:985 #: apps/wei/views.py:993
msgid "Please make sure the check is given before validating the registration" msgid "Please make sure the check is given before validating the registration"
msgstr "" msgstr ""
"Merci de vous assurer que le chèque a bien été donné avant de valider " "Merci de vous assurer que le chèque a bien été donné avant de valider "
"l'adhésion" "l'adhésion"
#: apps/wei/views.py:991 #: apps/wei/views.py:999
msgid "Create deposit transaction" msgid "Create deposit transaction"
msgstr "Créer une transaction de caution" msgstr "Créer une transaction de caution"
#: apps/wei/views.py:992 #: apps/wei/views.py:1000
#, python-format #, python-format
msgid "" msgid ""
"A transaction of %(amount).2f€ will be created from the user's Note account" "A transaction of %(amount).2f€ will be created from the user's Note account"
msgstr "" msgstr ""
"Un transaction de %(amount).2f€ va être créée depuis la note de l'utilisateur" "Un transaction de %(amount).2f€ va être créée depuis la note de l'utilisateur"
#: apps/wei/views.py:1087 #: apps/wei/views.py:1095
#, python-format #, python-format
msgid "" msgid ""
"This user doesn't have enough money to join this club and pay the deposit. " "This user doesn't have enough money to join this club and pay the deposit. "
@ -3698,21 +3723,21 @@ msgstr ""
"payer la cautionSolde actuel : %(balance)d€, crédit : %(credit)d€, requis : " "payer la cautionSolde actuel : %(balance)d€, crédit : %(credit)d€, requis : "
"%(needed)d€" "%(needed)d€"
#: apps/wei/views.py:1140 #: apps/wei/views.py:1148
#, fuzzy, python-format #, fuzzy, python-format
#| msgid "total amount" #| msgid "total amount"
msgid "Caution %(name)s" msgid "Caution %(name)s"
msgstr "montant total" msgstr "montant total"
#: apps/wei/views.py:1354 #: apps/wei/views.py:1362
msgid "Attribute buses to first year members" msgid "Attribute buses to first year members"
msgstr "Répartir les 1A dans les bus" msgstr "Répartir les 1A dans les bus"
#: apps/wei/views.py:1379 #: apps/wei/views.py:1388
msgid "Attribute bus" msgid "Attribute bus"
msgstr "Attribuer un bus" msgstr "Attribuer un bus"
#: apps/wei/views.py:1419 #: apps/wei/views.py:1428
msgid "" msgid ""
"No first year student without a bus found. Either all of them have a bus, or " "No first year student without a bus found. Either all of them have a bus, or "
"none has filled the survey yet." "none has filled the survey yet."
@ -3736,13 +3761,13 @@ msgstr "bde"
#: apps/wrapped/models.py:65 #: apps/wrapped/models.py:65
msgid "data json" msgid "data json"
msgstr "donnée json" msgstr "données json"
#: apps/wrapped/models.py:66 #: apps/wrapped/models.py:66
msgid "data in the wrapped and generated by the script generate_wrapped" msgid "data in the wrapped and generated by the script generate_wrapped"
msgstr "donnée dans le wrapped et générée par le script generate_wrapped" msgstr "donnée dans le wrapped et générée par le script generate_wrapped"
#: apps/wrapped/models.py:70 note_kfet/templates/base.html:114 #: apps/wrapped/models.py:70 note_kfet/templates/base.html:115
msgid "Wrapped" msgid "Wrapped"
msgstr "Wrapped" msgstr "Wrapped"
@ -3775,7 +3800,7 @@ msgid "Copy link"
msgstr "Copier le lien" msgstr "Copier le lien"
#: apps/wrapped/templates/wrapped/1/wrapped_base.html:16 #: apps/wrapped/templates/wrapped/1/wrapped_base.html:16
#: note_kfet/templates/base.html:14 #: note_kfet/templates/base.html:15
msgid "The ENS Paris-Saclay BDE note." msgid "The ENS Paris-Saclay BDE note."
msgstr "La note du BDE de l'ENS Paris-Saclay." msgstr "La note du BDE de l'ENS Paris-Saclay."
@ -3878,7 +3903,7 @@ msgid ""
"Do not forget to ask permission to people who are in your wrapped before to " "Do not forget to ask permission to people who are in your wrapped before to "
"make them public" "make them public"
msgstr "" msgstr ""
"N'oublies pas de demander la permission des personnes apparaissant dans un " "N'oublie pas de demander la permission des personnes apparaissant dans un "
"wrapped avant de le rendre public" "wrapped avant de le rendre public"
#: apps/wrapped/templates/wrapped/wrapped_list.html:40 #: apps/wrapped/templates/wrapped/wrapped_list.html:40
@ -3897,19 +3922,19 @@ msgstr "Le wrapped est public"
msgid "List of wrapped" msgid "List of wrapped"
msgstr "Liste des wrapped" msgstr "Liste des wrapped"
#: note_kfet/settings/base.py:177 #: note_kfet/settings/base.py:180
msgid "German" msgid "German"
msgstr "Allemand" msgstr "Allemand"
#: note_kfet/settings/base.py:178 #: note_kfet/settings/base.py:181
msgid "English" msgid "English"
msgstr "Anglais" msgstr "Anglais"
#: note_kfet/settings/base.py:179 #: note_kfet/settings/base.py:182
msgid "Spanish" msgid "Spanish"
msgstr "Espagnol" msgstr "Espagnol"
#: note_kfet/settings/base.py:180 #: note_kfet/settings/base.py:183
msgid "French" msgid "French"
msgstr "Français" msgstr "Français"
@ -3970,34 +3995,34 @@ msgstr ""
msgid "Reset" msgid "Reset"
msgstr "Réinitialiser" msgstr "Réinitialiser"
#: note_kfet/templates/base.html:84 #: note_kfet/templates/base.html:85
msgid "Users" msgid "Users"
msgstr "Utilisateur·rices" msgstr "Utilisateur·rices"
#: note_kfet/templates/base.html:90 #: note_kfet/templates/base.html:91
msgid "Clubs" msgid "Clubs"
msgstr "Clubs" msgstr "Clubs"
#: note_kfet/templates/base.html:125 #: note_kfet/templates/base.html:126
msgid "Admin" msgid "Admin"
msgstr "Admin" msgstr "Admin"
#: note_kfet/templates/base.html:139 #: note_kfet/templates/base.html:140
msgid "My account" msgid "My account"
msgstr "Mon compte" msgstr "Mon compte"
#: note_kfet/templates/base.html:142 #: note_kfet/templates/base.html:145
msgid "Log out" msgid "Log out"
msgstr "Se déconnecter" msgstr "Se déconnecter"
#: note_kfet/templates/base.html:150 #: note_kfet/templates/base.html:154
#: note_kfet/templates/registration/signup.html:6 #: note_kfet/templates/registration/signup.html:6
#: note_kfet/templates/registration/signup.html:11 #: note_kfet/templates/registration/signup.html:11
#: note_kfet/templates/registration/signup.html:28 #: note_kfet/templates/registration/signup.html:28
msgid "Sign up" msgid "Sign up"
msgstr "Inscription" msgstr "Inscription"
#: note_kfet/templates/base.html:157 #: note_kfet/templates/base.html:161
#: note_kfet/templates/registration/login.html:6 #: note_kfet/templates/registration/login.html:6
#: note_kfet/templates/registration/login.html:15 #: note_kfet/templates/registration/login.html:15
#: note_kfet/templates/registration/login.html:38 #: note_kfet/templates/registration/login.html:38
@ -4005,7 +4030,7 @@ msgstr "Inscription"
msgid "Log in" msgid "Log in"
msgstr "Se connecter" msgstr "Se connecter"
#: note_kfet/templates/base.html:171 #: note_kfet/templates/base.html:175
msgid "" msgid ""
"You are not a BDE member anymore. Please renew your membership if you want " "You are not a BDE member anymore. Please renew your membership if you want "
"to use the note." "to use the note."
@ -4013,7 +4038,7 @@ msgstr ""
"Vous n'êtes plus adhérent·e BDE. Merci de réadhérer si vous voulez profiter " "Vous n'êtes plus adhérent·e BDE. Merci de réadhérer si vous voulez profiter "
"de la note." "de la note."
#: note_kfet/templates/base.html:177 #: note_kfet/templates/base.html:181
msgid "" msgid ""
"Your e-mail address is not validated. Please check your mail inbox and click " "Your e-mail address is not validated. Please check your mail inbox and click "
"on the validation link." "on the validation link."
@ -4021,7 +4046,7 @@ msgstr ""
"Votre adresse e-mail n'est pas validée. Merci de vérifier votre boîte mail " "Votre adresse e-mail n'est pas validée. Merci de vérifier votre boîte mail "
"et de cliquer sur le lien de validation." "et de cliquer sur le lien de validation."
#: note_kfet/templates/base.html:183 #: note_kfet/templates/base.html:187
msgid "" msgid ""
"You declared that you opened a bank account in the Société générale. The " "You declared that you opened a bank account in the Société générale. The "
"bank did not validate the creation of the account to the BDE, so the " "bank did not validate the creation of the account to the BDE, so the "
@ -4035,22 +4060,38 @@ msgstr ""
"vérification peut durer quelques jours. Merci de vous assurer de bien aller " "vérification peut durer quelques jours. Merci de vous assurer de bien aller "
"au bout de vos démarches." "au bout de vos démarches."
#: note_kfet/templates/base.html:206 #: note_kfet/templates/base.html:214
msgid "Contact us" msgid "Contact us"
msgstr "Nous contacter" msgstr "Nous contacter"
#: note_kfet/templates/base.html:208 #: note_kfet/templates/base.html:216
msgid "Technical Support" msgid "Technical Support"
msgstr "Support technique" msgstr "Support technique"
#: note_kfet/templates/base.html:210 #: note_kfet/templates/base.html:218
msgid "Charte Info (FR)" msgid "Charte Info (FR)"
msgstr "Charte Info (FR)" msgstr "Charte Info (FR)"
#: note_kfet/templates/base.html:212 #: note_kfet/templates/base.html:220
msgid "FAQ (FR)" msgid "FAQ (FR)"
msgstr "FAQ (FR)" msgstr "FAQ (FR)"
#: note_kfet/templates/base.html:222
msgid "Managed by BDE"
msgstr "Géré par le BDE"
#: note_kfet/templates/base.html:224
msgid "Hosted by Cr@ns"
msgstr "Hébergé par le Cr@ans"
#: note_kfet/templates/base.html:266
msgid "The note is not available for now"
msgstr "La note est indisponible pour le moment"
#: note_kfet/templates/base.html:268
msgid "Thank you for your understanding -- The Respos Info of BDE"
msgstr "Merci de votre compréhension -- Les Respos Info du BDE"
#: note_kfet/templates/base_search.html:15 #: note_kfet/templates/base_search.html:15
msgid "Search by attribute such as name..." msgid "Search by attribute such as name..."
msgstr "Chercher par un attribut tel que le nom..." msgstr "Chercher par un attribut tel que le nom..."
@ -4059,6 +4100,41 @@ msgstr "Chercher par un attribut tel que le nom..."
msgid "There is no results." msgid "There is no results."
msgstr "Il n'y a pas de résultat." msgstr "Il n'y a pas de résultat."
#: note_kfet/templates/cas/logged.html:8
msgid ""
"<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!"
msgstr ""
"<h3>Connection réussie</h3>Vous vous êtes bien connecté au Service Central d'Authentification."
"<br/>Pour des raisons de sécurité, veuillez vous déconnecter et fermer votre navigateur internet "
"une fois que vous aurez fini d'accéder aux services qui requiert une authentification !"
#: note_kfet/templates/cas/logged.html:14
msgid "Log me out from all my sessions"
msgstr "Me déconnecter de toutes mes sessions"
#: note_kfet/templates/cas/logged.html:20
msgid "Forget the identity provider"
msgstr "Oublier le fournisseur d'identité"
#: note_kfet/templates/cas/logged.html:24
msgid "Logout"
msgstr "Déconnexion"
#: note_kfet/templates/cas/login.html:11
msgid "Please log in"
msgstr "Veuillez vous connecter"
#: note_kfet/templates/cas/login.html:23
msgid "Login"
msgstr "Connexion"
#: note_kfet/templates/cas/warn.html:14
msgid "Connect to the service"
msgstr "Connexion au service"
#: note_kfet/templates/oauth2_provider/application_confirm_delete.html:8 #: note_kfet/templates/oauth2_provider/application_confirm_delete.html:8
msgid "Are you sure to delete the application" msgid "Are you sure to delete the application"
msgstr "Êtes-vous sûr⋅e de vouloir supprimer l'application" msgstr "Êtes-vous sûr⋅e de vouloir supprimer l'application"
@ -4279,10 +4355,86 @@ msgstr ""
"d'adhésion. Vous devez également valider votre adresse email en suivant le " "d'adhésion. Vous devez également valider votre adresse email en suivant le "
"lien que vous avez reçu." "lien que vous avez reçu."
#, fuzzy, python-format #, fuzzy
#~| msgid "Creation date" #~| msgid "QR-code"
#~ msgid "Deposit %(name)s" #~ msgid "Go to QR-code"
#~ msgstr "Caution %(name)s" #~ msgstr "QR-code"
#, python-brace-format
#~ msgid "QR-code number {qr_code_number}"
#~ msgstr "Numéro du QR-code {qr_code_number}"
#~ msgid "was eaten"
#~ msgstr "a été mangé"
#~ msgid "is active"
#~ msgstr "est en cours"
#~ msgid "foods"
#~ msgstr "bouffes"
#~ msgid "Arrival date"
#~ msgstr "Date d'arrivée"
#~ msgid "Active"
#~ msgstr "Actif"
#~ msgid "Eaten"
#~ msgstr "Mangé"
#~ msgid "number"
#~ msgstr "numéro"
#~ msgid "View details"
#~ msgstr "Voir plus"
#~ msgid "Ready"
#~ msgstr "Prêt"
#~ msgid "Creation date"
#~ msgstr "Date de création"
#~ msgid "Ingredients"
#~ msgstr "Ingrédients"
#~ msgid "Open"
#~ msgstr "Open"
#~ msgid "All meals"
#~ msgstr "Tout les plats"
#~ msgid "There is no meal."
#~ msgstr "Il n'y a pas de plat"
#~ msgid "The product is already prepared"
#~ msgstr "Le produit est déjà prêt"
#~ msgid "Add a new basic food with QRCode"
#~ msgstr "Ajouter un nouvel ingrédient avec un QR-code"
#~ msgid "QRCode"
#~ msgstr "QR-code"
#~ msgid "Add a new meal"
#~ msgstr "Ajouter un nouveau plat"
#~ msgid "Update a meal"
#~ msgstr "Modifier le plat"
#, fuzzy
#~| msgid "invalidate"
#~ msgid "Enter a valid color."
#~ msgstr "dévalider"
#, fuzzy
#~| msgid "invalidate"
#~ msgid "Enter a valid value."
#~ msgstr "dévalider"
#, fuzzy
#~| msgid "Invitation"
#~ msgid "Syndication"
#~ msgstr "Invitation"
#, fuzzy #, fuzzy
#~| msgid "There is no results." #~| msgid "There is no results."
@ -4696,7 +4848,7 @@ msgstr ""
#, python-brace-format #, python-brace-format
#~ msgid "QR-code number {qr_code_number}" #~ msgid "QR-code number {qr_code_number}"
#~ msgstr "numéro du QR-code {qr_code_number}" #~ msgstr "Numéro du QR-code {qr_code_number}"
#~ msgid "was eaten" #~ msgid "was eaten"
#~ msgstr "a été mangé" #~ msgstr "a été mangé"

View File

@ -28,4 +28,5 @@ 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,3 +56,8 @@ 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

@ -39,7 +39,9 @@ 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',
@ -111,6 +113,7 @@ 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',
@ -307,6 +310,30 @@ 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,6 +5,7 @@ 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">
@ -138,9 +139,12 @@ 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>
<a class="dropdown-item" href="{% url 'logout' %}"> <form method="post" action="{% 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" %}
</a> </button>
</form>
</div> </div>
</li> </li>
{% else %} {% else %}
@ -188,7 +192,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endblocktrans %} {% endblocktrans %}
</div> </div>
{% endif %} {% endif %}
{# TODO Add banners #} {% if config.BANNER_MESSAGE and user.is_authenticated %}
<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>
@ -210,6 +218,10 @@ 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"
@ -246,4 +258,15 @@ 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

@ -0,0 +1,28 @@
{% 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

@ -0,0 +1,42 @@
{% 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

@ -0,0 +1,10 @@
{% 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

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

View File

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