1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-08-01 21:24:54 +02:00

Compare commits

...

14 Commits

Author SHA1 Message Date
Otthorn
0b8fa4001d Merge branch 'qrcode' into 'main'
Draft: Qrcode

See merge request bde/nk20!196
2025-07-25 03:16:49 +02:00
ehouarn
0992a8a7ee Merge branch 'wei' into 'main'
Wei

See merge request bde/nk20!334
2025-07-24 15:39:51 +02:00
Ehouarn
97597eb103 Fixed 1A forms 2025-07-24 12:26:44 +02:00
Ehouarn
bfa5734d55 Changed score calculation in survey 2025-07-23 16:48:59 +02:00
Ehouarn
296d021d54 Permissions 2025-07-23 01:24:59 +02:00
Ehouarn
6e348b995b Better Membership update 2025-07-23 00:51:03 +02:00
Ehouarn
1274315cde Last untranslated field 2025-07-19 18:55:49 +02:00
Nicolas Margulies
e6f3084588 Added a first pass for automatically entering an activity with a qrcode 2023-10-11 18:01:51 +02:00
otthorn
145e55da75 remove useless comment 2022-03-22 15:06:04 +01:00
otthorn
d3ba95cdca Insecable space for more clarity 2022-03-22 15:04:41 +01:00
otthorn
8ffb0ebb56 Use DetailView 2022-03-22 14:59:01 +01:00
otthorn
5038af9e34 Final html template 2022-03-22 14:58:26 +01:00
otthorn
819b4214c9 Add QRCode View, URL and test template 2022-03-22 12:26:44 +01:00
otthorn
b8a93b0b75 Add link to QR code 2022-03-19 16:25:15 +01:00
15 changed files with 447 additions and 74 deletions

View File

@@ -38,6 +38,7 @@ 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>
@@ -63,15 +64,46 @@ 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;
@@ -168,4 +200,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
});
}
</script>
{% endblock %}
{% endblock %}

View File

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

View File

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

View File

@@ -25,4 +25,5 @@ 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,6 +402,14 @@ 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

@@ -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

@@ -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': {
@@ -136,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 %}

View File

@@ -149,6 +149,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<li>{% blocktrans trimmed with amount=fee|pretty_money %}
Membership fees: {{ amount }}
{% endblocktrans %}</li>
{% if not registration.first_year %}
{% if registration.deposit_type == 'note' %}
<li>{% blocktrans trimmed with amount=club.deposit_amount|pretty_money %}
Deposit (by Note transaction): {{ amount }}
@@ -158,6 +159,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
Deposit (by check): {{ amount }}
{% endblocktrans %}</li>
{% endif %}
{% endif %}
<li><strong>{% blocktrans trimmed with total=total_needed|pretty_money %}
Total needed: {{ total }}
{% endblocktrans %}</strong></li>
@@ -213,7 +215,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

@@ -816,9 +816,12 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
del form.fields["deposit_check"]
# S'assurer que le champ deposit_type est obligatoire pour les 2A+
if not self.object.first_year and "deposit_type" in form.fields:
form.fields["deposit_type"].required = True
form.fields["deposit_type"].help_text = _("Choose how you want to pay the deposit")
if "deposit_type" in form.fields:
if self.object.first_year:
del form.fields["deposit_type"]
else:
form.fields["deposit_type"].required = True
form.fields["deposit_type"].help_text = _("Choose how you want to pay the deposit")
return form
@@ -879,7 +882,6 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]]
form.instance.information = information
# Sauvegarder le type de caution pour les 2A+
if "deposit_type" in form.cleaned_data:
form.instance.deposit_type = form.cleaned_data["deposit_type"]
form.instance.save()
@@ -1181,6 +1183,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.
@@ -1203,6 +1248,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,)))