1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-07-23 17:26:46 +02:00

Compare commits

..

7 Commits

Author SHA1 Message Date
bfa5734d55 Changed score calculation in survey 2025-07-23 16:48:59 +02:00
296d021d54 Permissions 2025-07-23 01:24:59 +02:00
6e348b995b Better Membership update 2025-07-23 00:51:03 +02:00
1274315cde Last untranslated field 2025-07-19 18:55:49 +02:00
2755a5f7ab Minor fail 2025-07-19 17:10:25 +02:00
9ab4df94e6 Minor fixes 2025-07-19 16:55:07 +02:00
edb6abfff5 Add fee field to WEIRegistration to be able to sort on validation status 2025-07-19 16:24:25 +02:00
21 changed files with 507 additions and 208 deletions

View File

@ -38,7 +38,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
</a>
<input id="alias" type="text" class="form-control" placeholder="Nom/note ...">
<button id="trigger" class="btn btn-secondary">Click me !</button>
<hr>
@ -64,46 +63,15 @@ SPDX-License-Identifier: GPL-3.0-or-later
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) {
let code = event.originalEvent.keyCode
if (65 <= code <= 122 || code === 13) {
debounce(reloadTable)()
}
if (code === 0)
process_qrcode();
});
$(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() {
$(".table-row").click(function (e) {
let target = e.target.parentElement;

View File

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

@ -13,3 +13,25 @@ def save_user_profile(instance, created, raw, **_kwargs):
instance.profile.email_confirmed = True
instance.profile.registration_valid = True
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,10 +60,7 @@
{% if user_object.pk == user.pk %}
<div class="text-center">
<a class="small badge badge-secondary" href="{% url 'member:auth_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' %}
<i class="fa fa-cogs"></i>{% trans 'API token' %}
</a>
</div>
{% endif %}

View File

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

@ -25,5 +25,4 @@ urlpatterns = [
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('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,14 +402,6 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
context['token'] = Token.objects.get_or_create(user=self.request.user)[0]
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 #

View File

@ -4347,7 +4347,23 @@
"mask": 3,
"field": "",
"permanent": false,
"description": "Ajouter un membre au BDE ou à la Kfet"
"description": "Faire adhérer BDE ou Kfet"
}
},
{
"model": "permission.permission",
"pk": 293,
"fields": {
"model": [
"wei",
"weimembership"
],
"query": "[\"AND\", {\"bus\": [\"membership\", \"weimembership\", \"bus\"]}, {\"club\": [\"club\"], \"club__weiclub__membership_end__gte\": [\"today\"]}]",
"type": "change",
"mask": 2,
"field": "team",
"permanent": false,
"description": "Modifier l'équipe d'une adhésion WEI à son bus"
}
},
{
@ -4764,7 +4780,6 @@
"name": "Chef\u22c5fe de bus",
"permissions": [
22,
84,
115,
117,
118,
@ -4778,7 +4793,8 @@
287,
289,
290,
291
291,
293
]
}
},
@ -4790,7 +4806,6 @@
"name": "Chef\u22c5fe d'\u00e9quipe",
"permissions": [
22,
84,
116,
123,
124,
@ -4805,8 +4820,7 @@
"for_club": null,
"name": "\u00c9lectron libre",
"permissions": [
22,
84
22
]
}
},
@ -4957,7 +4971,6 @@
"name": "Référent⋅e Bus",
"permissions": [
22,
84,
115,
117,
118,
@ -4971,7 +4984,8 @@
287,
289,
290,
291
291,
293
]
}
},

View File

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

View File

@ -44,7 +44,7 @@ class WEIRegistrationForm(forms.ModelForm):
fields = [
'user', 'soge_credit', 'birth_date', 'gender', 'clothing_size',
'health_issues', 'emergency_contact_name', 'emergency_contact_phone',
'first_year', 'information_json', 'deposit_check'
'first_year', 'information_json', 'deposit_check', 'deposit_type'
]
widgets = {
"user": Autocomplete(
@ -62,21 +62,8 @@ class WEIRegistrationForm(forms.ModelForm):
"deposit_check": forms.BooleanField(
required=False,
),
}
class WEIRegistration2AForm(WEIRegistrationForm):
class Meta(WEIRegistrationForm.Meta):
fields = WEIRegistrationForm.Meta.fields + ['deposit_type']
widgets = WEIRegistrationForm.Meta.widgets.copy()
widgets.update({
"deposit_type": forms.RadioSelect(),
})
class WEIRegistration1AForm(WEIRegistrationForm):
class Meta(WEIRegistrationForm.Meta):
fields = WEIRegistrationForm.Meta.fields
}
class WEIChooseBusForm(forms.Form):

View File

@ -14,16 +14,139 @@ 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',
]
WORDS = {
'list': [
'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',
],
'questions': {
'Question 1': [
'Description 1',
{
3: 'Réponse 1 Madagas[car]',
43: 'Réponse 1 Y2[KAR]',
2: 'Réponse 1 Tcherno[bus]',
45: 'Réponse 1 [Kar]tier',
1: 'Réponse 1 [Car]cassonne',
47: 'Réponse 1 O[car]ina',
48: 'Réponse 1 Show[bus]',
49: 'Réponse 1 [Car]ioca'
}
],
'Question 2': [
'Description 2',
{
3: 'Réponse 2 Madagas[car]',
43: 'Réponse 2 Y2[KAR]',
2: 'Réponse 2 Tcherno[bus]',
45: 'Réponse 2 [Kar]tier',
1: 'Réponse 2 [Car]cassonne',
47: 'Réponse 2 O[car]ina',
48: 'Réponse 2 Show[bus]',
49: 'Réponse 2 [Car]ioca'
}
],
'Question 3': [
'Description 3',
{
3: 'Réponse 3 Madagas[car]',
43: 'Réponse 3 Y2[KAR]',
2: 'Réponse 3 Tcherno[bus]',
45: 'Réponse 3 [Kar]tier',
1: 'Réponse 3 [Car]cassonne',
47: 'Réponse 3 O[car]ina',
48: 'Réponse 3 Show[bus]',
49: 'Réponse 3 [Car]ioca'
}
],
'Question 4': [
'Description 4',
{
3: 'Réponse 4 Madagas[car]',
43: 'Réponse 4 Y2[KAR]',
2: 'Réponse 4 Tcherno[bus]',
45: 'Réponse 4 [Kar]tier',
1: 'Réponse 4 [Car]cassonne',
47: 'Réponse 4 O[car]ina',
48: 'Réponse 4 Show[bus]',
49: 'Réponse 4 [Car]ioca'
}
],
'Question 5': [
'Description 5',
{
3: 'Réponse 5 Madagas[car]',
43: 'Réponse 5 Y2[KAR]',
2: 'Réponse 5 Tcherno[bus]',
45: 'Réponse 5 [Kar]tier',
1: 'Réponse 5 [Car]cassonne',
47: 'Réponse 5 O[car]ina',
48: 'Réponse 5 Show[bus]',
49: 'Réponse 5 [Car]ioca'
}
],
'Question 6': [
'Description 6',
{
3: 'Réponse 6 Madagas[car]',
43: 'Réponse 6 Y2[KAR]',
2: 'Réponse 6 Tcherno[bus]',
45: 'Réponse 6 [Kar]tier',
1: 'Réponse 6 [Car]cassonne',
47: 'Réponse 6 O[car]ina',
48: 'Réponse 6 Show[bus]',
49: 'Réponse 6 [Car]ioca'
}
],
'Question 7': [
'Description 7',
{
3: 'Réponse 7 Madagas[car]',
43: 'Réponse 7 Y2[KAR]',
2: 'Réponse 7 Tcherno[bus]',
45: 'Réponse 7 [Kar]tier',
1: 'Réponse 7 [Car]cassonne',
47: 'Réponse 7 O[car]ina',
48: 'Réponse 7 Show[bus]',
49: 'Réponse 7 [Car]ioca'
}
],
'Question 8': [
'Description 8',
{
3: 'Réponse 8 Madagas[car]',
43: 'Réponse 8 Y2[KAR]',
2: 'Réponse 8 Tcherno[bus]',
45: 'Réponse 8 [Kar]tier',
1: 'Réponse 8 [Car]cassonne',
47: 'Réponse 8 O[car]ina',
48: 'Réponse 8 Show[bus]',
49: 'Réponse 8 [Car]ioca'
}
],
'Question 9': [
'Description 9',
{
3: 'Réponse 9 Madagas[car]',
43: 'Réponse 9 Y2[KAR]',
2: 'Réponse 9 Tcherno[bus]',
45: 'Réponse 9 [Kar]tier',
1: 'Réponse 9 [Car]cassonne',
47: 'Réponse 9 O[car]ina',
48: 'Réponse 9 Show[bus]',
49: 'Réponse 9 [Car]ioca'
}
]
}
}
NB_WORDS = 5
class WEISurveyForm2025(forms.Form):
@ -32,11 +155,6 @@ class WEISurveyForm2025(forms.Form):
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.
@ -48,34 +166,56 @@ class WEISurveyForm2025(forms.Form):
registration._force_save = True
registration.save()
if self.data:
self.fields["word"].choices = [(w, w) for w in WORDS]
rng = Random((information.step + 1) * information.seed)
if information.step == 0:
self.fields["words"] = forms.MultipleChoiceField(
label=_(f"Choose {NB_WORDS} words:"),
choices=[(w, w) for w in WORDS['list']],
widget=forms.CheckboxSelectMultiple(),
required=True,
)
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
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)
preferred_words = {
bus: [word for word in WORDS['list'] if informations[bus].scores[word] >= average_score]
for bus in buses
}
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)
self.fields["words"].choices = [(w, w) for w in all_preferred_words]
else:
average_score = 0
questions = list(WORDS['questions'].items())
idx = information.step - 1
if idx < len(questions):
q, (desc, answers) = questions[idx]
choices = [(k, v) for k, v in answers.items()]
rng.shuffle(choices)
self.fields[q] = forms.ChoiceField(
label=desc,
choices=choices,
widget=forms.RadioSelect,
required=True,
)
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]
def clean_words(self):
data = self.cleaned_data['words']
if len(data) != NB_WORDS:
raise forms.ValidationError(_(f"Please choose exactly {NB_WORDS} words"))
return data
class WEIBusInformation2025(WEIBusInformation):
@ -86,7 +226,7 @@ class WEIBusInformation2025(WEIBusInformation):
def __init__(self, bus):
self.scores = {}
for word in WORDS:
for word in WORDS['list']:
self.scores[word] = 0
super().__init__(bus)
@ -108,7 +248,7 @@ class BusInformationForm2025(forms.ModelForm):
except (json.JSONDecodeError, TypeError, AttributeError):
initial_scores = {}
if words is None:
words = WORDS
words = WORDS['list']
self.words = words
choices = [(i, str(i)) for i in range(6)] # [(0, '0'), (1, '1'), ..., (5, '5')]
@ -145,10 +285,26 @@ class WEISurveyInformation2025(WEISurveyInformation):
step = 0
def __init__(self, registration):
for i in range(1, 21):
for i in range(1, 5):
setattr(self, "word" + str(i), None)
for q in WORDS['questions']:
setattr(self, q, None)
super().__init__(registration)
def reset(self, registration):
"""
Réinitialise complètement le questionnaire : step, seed, mots choisis et réponses aux questions.
"""
self.step = 0
self.seed = 0
for i in range(1, 5):
setattr(self, f"word{i}", None)
for q in WORDS['questions']:
setattr(self, q, None)
self.save(registration)
registration._force_save = True
registration.save()
class WEISurvey2025(WEISurvey):
"""
@ -174,10 +330,20 @@ class WEISurvey2025(WEISurvey):
@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()
if self.information.step == 0:
words = form.cleaned_data['words']
for i, word in enumerate(words, 1):
setattr(self.information, "word" + str(i), word)
self.information.step += 1
self.save()
else:
questions = list(WORDS['questions'].keys())
idx = self.information.step - 1
if idx < len(questions):
q = questions[idx]
setattr(self.information, q, form.cleaned_data[q])
self.information.step += 1
self.save()
@classmethod
def get_algorithm_class(cls):
@ -187,7 +353,7 @@ class WEISurvey2025(WEISurvey):
"""
The survey is complete once the bus is chosen.
"""
return self.information.step == 20
return self.information.step > len(WORDS['questions'])
@classmethod
@lru_cache()
@ -206,7 +372,8 @@ class WEISurvey2025(WEISurvey):
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
- self.word_mean(getattr(self.information, 'word' + str(i))) for i in range(1, 1 + NB_WORDS)) / NB_WORDS
s += sum(1 for q in WORDS['questions'] if getattr(self.information, q) == str(bus.pk))
return s
@lru_cache()
@ -243,6 +410,13 @@ class WEISurveyAlgorithm2025(WEISurveyAlgorithm):
def get_bus_information_form(cls):
return BusInformationForm2025
@classmethod
def get_buses(cls):
if not hasattr(cls, '_buses'):
cls._buses = Bus.objects.filter(wei__year=cls.get_survey_class().get_year(), size__gt=0).all().exclude(name='Staff')
return cls._buses
def run_algorithm(self, display_tqdm=False):
"""
Gale-Shapley algorithm implementation.

View File

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

@ -285,6 +285,12 @@ class WEIRegistration(models.Model):
"encoded in JSON"),
)
fee = models.PositiveIntegerField(
default=0,
verbose_name=_('fee'),
blank=True,
)
class Meta:
unique_together = ('user', 'wei',)
verbose_name = _("WEI User")
@ -309,7 +315,25 @@ class WEIRegistration(models.Model):
self.information_json = json.dumps(information, indent=2)
@property
def fee(self):
def is_validated(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)
kfet = Club.objects.get(pk=2)
@ -336,12 +360,9 @@ class WEIRegistration(models.Model):
return fee
@property
def is_validated(self):
try:
return self.membership is not None
except AttributeError:
return False
def save(self, *args, **kwargs):
self.fee = self.calculate_fee()
super().save(*args, **kwargs)
class WEIMembership(Membership):

View File

@ -58,8 +58,8 @@ class WEIRegistrationTable(tables.Table):
validate = tables.Column(
verbose_name=_("Validate"),
orderable=False,
accessor=A('pk'),
orderable=True,
accessor='validate_status',
attrs={
'th': {
'id': 'validate-membership-header'
@ -71,7 +71,7 @@ class WEIRegistrationTable(tables.Table):
'wei:wei_delete_registration',
args=[A('pk')],
orderable=False,
verbose_name=_("delete"),
verbose_name=_("Delete"),
text=_("Delete"),
attrs={
'th': {
@ -98,12 +98,13 @@ class WEIRegistrationTable(tables.Table):
if not hasperm:
return format_html("<span class='no-perm'></span>")
url = reverse_lazy('wei:wei_update_registration', args=(record.pk,)) + '?validate=true'
url = reverse_lazy('wei:validate_registration', args=(record.pk,))
text = _('Validate')
if record.fee > record.user.note.balance and not record.soge_credit:
status = record.validation_status
if status == 2:
btn_class = 'btn-secondary'
tooltip = _("The user does not have enough money.")
elif record.first_year:
elif status == 1:
btn_class = 'btn-info'
tooltip = _("The user is in first year. You may validate the credit, the algorithm will run later.")
else:
@ -121,6 +122,7 @@ class WEIRegistrationTable(tables.Table):
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
order_by = ('validate', 'user',)
model = WEIRegistration
template_name = 'django_tables2/bootstrap4.html'
fields = ('user', 'user__first_name', 'user__last_name', 'first_year', 'deposit_check',
@ -134,8 +136,8 @@ class WEIRegistrationTable(tables.Table):
class WEIMembershipTable(tables.Table):
user = tables.LinkColumn(
'wei:wei_update_registration',
args=[A('registration__pk')],
'wei:wei_update_membership',
args=[A('pk')],
)
year = tables.Column(

View File

@ -50,7 +50,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endif %}
{% if club.deposit_amount > 0 %}
<dt class="col-xl-6">{% trans 'Deposit amount'|capfirst %}</dt>
<dt class="col-xl-6">{% trans 'deposit amount'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.deposit_amount|pretty_money }}</dd>
{% endif %}

View File

@ -39,6 +39,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
data-turbolinks="false">
{% trans "Update my registration" %}
</a>
{% if not not_first_year %}
<a class="btn btn-warning" href="{% url "wei:wei_survey" pk=my_registration.pk %}?reset=true" data-turbolinks="false">
{% trans "Restart survey" %}
</a>
{% endif %}
{% endif %}
</div>
{% endif %}
@ -67,20 +72,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div>
{% 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 %}
<div class="card bg-white mb-3">
<div class="card-header position-relative" id="historyListHeading">
@ -99,6 +90,19 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a href="{% url 'wei:wei_1A_list' pk=object.pk %}" class="btn btn-block btn-info">{% trans "Attribute buses" %}</a>
{% 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 %}

View File

@ -213,7 +213,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
$("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');

View File

@ -0,0 +1,46 @@
{% extends "base.html" %}
{% comment %}
Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n crispy_forms_tags %}
{% block content %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
{{ title }}
</h3>
<div class="card-body" id="form">
<form method="post">
{% csrf_token %}
{{ form | crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit"%}</button>
</form>
</div>
</div>
{% endblock %}
{% block extrajavascript %}
<script>
$(document).ready(function () {
function refreshTeams() {
let buses = [];
$("input[name='bus']:checked").each(function (ignored) {
buses.push($(this).parent().text().trim());
});
$("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 %}

View File

@ -6,7 +6,7 @@ import random
from django.contrib.auth.models import User
from django.test import TestCase
from ..forms.surveys.wei2025 import WEIBusInformation2025, WEISurvey2025, WORDS, WEISurveyInformation2025
from ..forms.surveys.wei2025 import WEIBusInformation2025, WEISurvey2025, WORDS, NB_WORDS, WEISurveyInformation2025
from ..models import Bus, WEIClub, WEIRegistration
@ -34,8 +34,8 @@ class TestWEIAlgorithm(TestCase):
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)
for word in WORDS['list']:
information.scores[word] = random.randint(0, 6)
information.save()
bus.save()
@ -54,7 +54,7 @@ class TestWEIAlgorithm(TestCase):
)
information = WEISurveyInformation2025(registration)
for j in range(1, 21):
setattr(information, f'word{j}', random.choice(WORDS))
setattr(information, f'word{j}', random.choice(WORDS['list']))
information.step = 20
information.save(registration)
registration.save()
@ -83,9 +83,11 @@ class TestWEIAlgorithm(TestCase):
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
for j in range(1, 1 + NB_WORDS):
setattr(information, f'word{j}', random.choice(WORDS['list']))
for q in WORDS['questions']:
setattr(information, q, random.choice(list(WORDS['questions'][q][1].keys())))
information.step = len(WORDS['questions']) + 1
information.save(registration)
registration.save()
@ -106,6 +108,6 @@ class TestWEIAlgorithm(TestCase):
max_score = buses[0][1]
penalty += (max_score - score) ** 2
self.assertLessEqual(max_score - score, 25) # Always less than 25 % of tolerance
self.assertLessEqual(max_score - score, 1) # Always less than 25 % of tolerance
self.assertLessEqual(penalty / 100, 25) # Tolerance of 5 %

View File

@ -7,7 +7,7 @@ from .views import CurrentWEIDetailView, WEI1AListView, WEIListView, WEICreateVi
WEIRegistrationsView, WEIMembershipsView, MemberListRenderView, BusInformationUpdateView, \
BusCreateView, BusManageView, BusUpdateView, BusTeamCreateView, BusTeamManageView, BusTeamUpdateView, \
WEIAttributeBus1AView, WEIAttributeBus1ANextView, WEIRegister1AView, WEIRegister2AView, WEIUpdateRegistrationView, \
WEIDeleteRegistrationView, WEIValidateRegistrationView, WEISurveyView, WEISurveyEndView, WEIClosedView
WEIDeleteRegistrationView, WEIValidateRegistrationView, WEISurveyView, WEISurveyEndView, WEIClosedView, WEIUpdateMembershipView
app_name = 'wei'
urlpatterns = [
@ -43,4 +43,6 @@ urlpatterns = [
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('update-bus-info/<int:pk>/', BusInformationUpdateView.as_view(), name="update_bus_info"),
path('edit_membership/<int:pk>/', WEIUpdateMembershipView.as_view(), name="wei_update_membership"),
]

View File

@ -13,7 +13,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import Q, Count
from django.db.models import Q, Count, Case, When, Value, IntegerField, F
from django.db.models.functions.text import Lower
from django import forms
from django.http import HttpResponse, Http404
@ -35,7 +35,7 @@ from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from .forms.registration import WEIChooseBusForm
from .models import WEIClub, WEIRegistration, WEIMembership, Bus, BusTeam, WEIRole
from .forms import WEIForm, WEIRegistrationForm, WEIRegistration1AForm, WEIRegistration2AForm, BusForm, BusTeamForm, WEIMembership1AForm, \
from .forms import WEIForm, WEIRegistrationForm, BusForm, BusTeamForm, WEIMembership1AForm, \
WEIMembershipForm, CurrentSurvey
from .tables import BusRepartitionTable, BusTable, BusTeamTable, WEITable, WEIRegistrationTable, \
WEIRegistration1ATable, WEIMembershipTable
@ -133,6 +133,23 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, D
membership=None,
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")) \
.filter(wei=self.object).annotate(count=Count("memberships")).order_by("name")
return [club_transactions, club_member, pre_registrations, buses, ]
@ -260,6 +277,23 @@ class WEIRegistrationsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTable
def get_queryset(self, **kwargs):
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", "")
@ -510,7 +544,7 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView):
Register a new user to the WEI
"""
model = WEIRegistration
form_class = WEIRegistration1AForm
form_class = WEIRegistrationForm
extra_context = {"title": _("Register first year student to the WEI")}
def get_sample_object(self):
@ -606,7 +640,7 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
Register an old user to the WEI
"""
model = WEIRegistration
form_class = WEIRegistration2AForm
form_class = WEIRegistrationForm
extra_context = {"title": _("Register old student to the WEI")}
def get_sample_object(self):
@ -739,14 +773,11 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
if today >= wei.date_start or today < wei.membership_start:
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
# Store the validate parameter in the view's state
self.should_validate = request.GET.get('validate', False)
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["club"] = self.object.wei
# Pass the validate parameter to the template
context["should_validate"] = self.should_validate
if self.object.is_validated:
membership_form = self.get_membership_form(instance=self.object.membership,
@ -788,7 +819,6 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
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")
form.fields["deposit_type"].widget = forms.RadioSelect(choices=form.fields["deposit_type"].choices)
return form
@ -862,9 +892,6 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
survey = CurrentSurvey(self.object)
if not survey.is_complete():
return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk})
# On redirige vers la validation uniquement si c'est explicitement demandé (et stocké dans la vue)
if self.should_validate and self.request.user.has_perm("wei.add_weimembership"):
return reverse_lazy("wei:validate_registration", kwargs={"pk": self.object.pk})
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.wei.pk})
@ -963,9 +990,9 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
form = context["form"]
if registration.soge_credit:
form.fields["credit_amount"].initial = registration.fee
form.fields["credit_amount"].initial = fee
else:
form.fields["credit_amount"].initial = max(0, registration.fee - registration.user.note.balance)
form.fields["credit_amount"].initial = max(0, fee - registration.user.note.balance)
return context
@ -1052,7 +1079,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
fee = club.membership_fee_paid if user.profile.paid else club.membership_fee_unpaid
if registration.soge_credit:
fee = 2000
fee = registration.wei.fee_soge_credit
kfet = club.parent_club
bde = kfet.parent_club
@ -1154,6 +1181,49 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
return reverse_lazy("wei:wei_registrations", kwargs={"pk": self.object.club.pk})
class WEIUpdateMembershipView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
Update a membership for the WEI
"""
model = WEIMembership
context_object_name = "membership"
template_name = "wei/weimembership_update.html"
extra_context = {"title": _("Update WEI Membership")}
def dispatch(self, request, *args, **kwargs):
wei = self.get_object().registration.wei
today = date.today()
# 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:
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)
def get_form(self):
form = WEIMembershipForm(
self.request.POST or None,
self.request.FILES or None,
instance=self.object,
wei=self.object.registration.wei,
)
form.fields["roles"].initial = self.object.roles.all()
form.fields["bus"].initial = self.object.bus
form.fields["team"].initial = self.object.team
del form.fields["credit_type"]
del form.fields["credit_amount"]
del form.fields["first_name"]
del form.fields["last_name"]
del form.fields["bank"]
return form
def get_success_url(self):
print("get_success_url")
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.registration.wei.pk})
class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView):
"""
Display the survey for the WEI for first year members.
@ -1176,6 +1246,10 @@ class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView):
if not self.survey:
self.survey = CurrentSurvey(obj)
if request.GET.get("reset") == "true":
info = self.survey.information
info.reset(obj)
# If the survey is complete, then display the end page.
if self.survey.is_complete():
return redirect(reverse_lazy('wei:wei_survey_end', args=(self.survey.registration.pk,)))