Compare commits

...

25 Commits

Author SHA1 Message Date
Otthorn 92b2c4690f Merge branch 'qrcode' into 'main'
Draft: Qrcode

See merge request bde/nk20!196
2024-08-30 15:24:47 +02:00
mcngnt 21cbf2b21a Merge branch 'survey_wei_2024' into 'main'
Survey wei 2024

See merge request bde/nk20!269
2024-08-29 23:10:57 +02:00
mcngnt 185a2cabf2 corrected emoji + linting 2024-08-29 22:47:33 +02:00
mcngnt 7552e55c8d removed diet filed 2024-08-29 22:19:11 +02:00
mcngnt e4aa16986f Merge branch 'survey_wei_2024' into 'main'
linting

See merge request bde/nk20!267
2024-08-29 19:12:23 +02:00
mcngnt b92e6e4e10 linting 2024-08-29 18:36:20 +02:00
mcngnt dd675b3676 Merge branch 'survey_wei_2024' into 'main'
Survey wei 2024

See merge request bde/nk20!266
2024-08-29 14:45:28 +02:00
mcngnt f50849b4f8 delete print 2024-08-29 14:01:55 +02:00
mcngnt 73ff35c232 updated bus descr 2024-08-29 12:42:26 +02:00
korenstin a5df98224f Merge branch 'migration-django-4-2' into 'main'
Migration django 4 2

See merge request bde/nk20!265
2024-08-29 10:49:44 +02:00
korenstin 2cb9ac8735 replace "…" -> "..." (#130) and disable sorting on certain columns (#129) 2024-08-29 10:19:06 +02:00
korenstin 35d4849a28 fix Oauth 2024-08-29 00:43:33 +02:00
mcngnt 96539d262f working html for survey + fixed json error + added specific diet text field 2024-08-29 00:05:44 +02:00
korenstin 946674f59b inclusif, avoids python3.10 syntax 2024-08-28 11:11:32 +02:00
mcngnt a201d8376a updated survey 2024-08-28 11:01:33 +02:00
korenstin a21b9275ea Add caution_check in the validation form, #96 2024-08-28 09:48:52 +02:00
korenstin d4e85e8215 test wei 2024, linters 2024-08-28 09:48:52 +02:00
mcngnt 7af2ebba40 basic survey 2024-08-28 09:48:52 +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
35 changed files with 817 additions and 93 deletions

View File

@ -0,0 +1,24 @@
# Generated by Django 4.2.15 on 2024-08-28 08:00
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('note', '0006_trust'),
('activity', '0004_opener'),
]
operations = [
migrations.AlterModelOptions(
name='opener',
options={'verbose_name': 'Opener', 'verbose_name_plural': 'Openers'},
),
migrations.AlterField(
model_name='opener',
name='opener',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activity_responsible', to='note.note', verbose_name='Opener'),
),
]

View File

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

View File

@ -0,0 +1,20 @@
# Generated by Django 4.2.15 on 2024-08-28 08:00
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('food', '0004_auto_20240813_2358'),
]
operations = [
migrations.AlterField(
model_name='food',
name='polymorphic_ctype',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'),
),
]

View File

@ -42,12 +42,12 @@ class UserTable(tables.Table):
""" """
alias = tables.Column() alias = tables.Column()
section = tables.Column(accessor='profile__section') section = tables.Column(accessor='profile__section', orderable=False)
# Override the column to let replace the URL # Override the column to let replace the URL
email = tables.EmailColumn(linkify=lambda record: "mailto:{}".format(record.email)) email = tables.EmailColumn(linkify=lambda record: "mailto:{}".format(record.email))
balance = tables.Column(accessor='note__balance', verbose_name=_("Balance")) balance = tables.Column(accessor='note__balance', verbose_name=_("Balance"), orderable=False)
def render_email(self, record, value): def render_email(self, record, value):
# Replace the email by a dash if the user can't see the profile detail # Replace the email by a dash if the user can't see the profile detail

View File

@ -11,7 +11,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{{ title }} {{ title }}
</h3> </h3>
<div class="card-body"> <div class="card-body">
<input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note"> <input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note...">
<div class="form-check"> <div class="form-check">
<label class="form-check-label" for="only_active"> <label class="form-check-label" for="only_active">
<input type="checkbox" class="checkboxinput form-check-input" id="only_active" <input type="checkbox" class="checkboxinput form-check-input" id="only_active"
@ -66,4 +66,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
roles_obj.change(reloadTable); roles_obj.change(reloadTable);
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,25 @@
# Generated by Django 4.2.15 on 2024-08-28 08:00
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('note', '0006_trust'),
]
operations = [
migrations.AlterField(
model_name='note',
name='polymorphic_ctype',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'),
),
migrations.AlterField(
model_name='transaction',
name='polymorphic_ctype',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'),
),
]

View File

@ -260,11 +260,13 @@ class ButtonTable(tables.Table):
text=_('edit'), text=_('edit'),
accessor='pk', accessor='pk',
verbose_name=_("Edit"), verbose_name=_("Edit"),
orderable=False,
) )
hideshow = tables.Column( hideshow = tables.Column(
verbose_name=_("Hide/Show"), verbose_name=_("Hide/Show"),
accessor="pk", accessor="pk",
orderable=False,
attrs={ attrs={
'td': { 'td': {
'class': 'col-sm-1', 'class': 'col-sm-1',
@ -276,7 +278,8 @@ class ButtonTable(tables.Table):
delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE, delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
extra_context={"delete_trans": _('delete')}, extra_context={"delete_trans": _('delete')},
attrs={'td': {'class': 'col-sm-1'}}, attrs={'td': {'class': 'col-sm-1'}},
verbose_name=_("Delete"), ) verbose_name=_("Delete"),
orderable=False, )
def render_amount(self, value): def render_amount(self, value):
return pretty_money(value) return pretty_money(value)

View File

@ -135,18 +135,18 @@ class Permission(models.Model):
# A json encoded Q object with the following grammar # A json encoded Q object with the following grammar
# query -> [] | {} (the empty query representing all objects) # query -> [] | {} (the empty query representing all objects)
# query -> ["AND", query, …] AND multiple queries # query -> ["AND", query, ...] AND multiple queries
# | ["OR", query, …] OR multiple queries # | ["OR", query, ...] OR multiple queries
# | ["NOT", query] Opposite of query # | ["NOT", query] Opposite of query
# query -> {key: value, …} A list of fields and values of a Q object # query -> {key: value, ...} A list of fields and values of a Q object
# key -> string A field name # key -> string A field name
# value -> int | string | bool | null Literal values # value -> int | string | bool | null Literal values
# | [parameter, …] A parameter. See compute_param for more details. # | [parameter, ...] A parameter. See compute_param for more details.
# | {"F": oper} An F object # | {"F": oper} An F object
# oper -> [string, …] A parameter. See compute_param for more details. # oper -> [string, ...] A parameter. See compute_param for more details.
# | ["ADD", oper, …] Sum multiple F objects or literal # | ["ADD", oper, ...] Sum multiple F objects or literal
# | ["SUB", oper, oper] Substract two F objects or literal # | ["SUB", oper, oper] Substract two F objects or literal
# | ["MUL", oper, …] Multiply F objects or literals # | ["MUL", oper, ...] Multiply F objects or literals
# | int | string | bool | null Literal values # | int | string | bool | null Literal values
# | ["F", string] A field # | ["F", string] A field
# #

View File

@ -0,0 +1,19 @@
# Generated by Django 4.2.15 on 2024-08-28 08:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('note', '0007_alter_note_polymorphic_ctype_and_more'),
('treasury', '0008_auto_20240322_0045'),
]
operations = [
migrations.AlterField(
model_name='sogecredit',
name='transactions',
field=models.ManyToManyField(blank=True, related_name='+', to='note.membershiptransaction', verbose_name='membership transactions'),
),
]

View File

@ -37,6 +37,7 @@ class InvoiceTable(tables.Table):
args=[A('id')], args=[A('id')],
verbose_name=_("delete"), verbose_name=_("delete"),
text=_("Delete"), text=_("Delete"),
orderable=False,
attrs={ attrs={
'th': { 'th': {
'id': 'delete-membership-header' 'id': 'delete-membership-header'
@ -70,6 +71,7 @@ class RemittanceTable(tables.Table):
verbose_name=_("View"), verbose_name=_("View"),
args=[A("pk")], args=[A("pk")],
text=_("View"), text=_("View"),
orderable=False,
attrs={ attrs={
'a': {'class': 'btn btn-primary'} 'a': {'class': 'btn btn-primary'}
}, ) }, )
@ -97,6 +99,7 @@ class SpecialTransactionTable(tables.Table):
verbose_name=_("Remittance"), verbose_name=_("Remittance"),
args=[A("specialtransactionproxy__pk")], args=[A("specialtransactionproxy__pk")],
text=_("Add"), text=_("Add"),
orderable=False,
attrs={ attrs={
'a': {'class': 'btn btn-primary'} 'a': {'class': 'btn btn-primary'}
}, ) }, )
@ -105,6 +108,7 @@ class SpecialTransactionTable(tables.Table):
verbose_name=_("Remittance"), verbose_name=_("Remittance"),
args=[A("specialtransactionproxy__pk")], args=[A("specialtransactionproxy__pk")],
text=_("Remove"), text=_("Remove"),
orderable=False,
attrs={ attrs={
'a': {'class': 'btn btn-primary btn-danger'} 'a': {'class': 'btn btn-primary btn-danger'}
}, ) }, )
@ -130,10 +134,12 @@ class SogeCreditTable(tables.Table):
amount = tables.Column( amount = tables.Column(
verbose_name=_("Amount"), verbose_name=_("Amount"),
orderable=False,
) )
valid = tables.Column( valid = tables.Column(
verbose_name=_("Valid"), verbose_name=_("Valid"),
orderable=False,
) )
def render_amount(self, value): def render_amount(self, value):

View File

@ -81,6 +81,11 @@ class WEIChooseBusForm(forms.Form):
class WEIMembershipForm(forms.ModelForm): class WEIMembershipForm(forms.ModelForm):
caution_check = forms.BooleanField(
required=False,
label=_("Caution check given"),
)
roles = forms.ModelMultipleChoiceField( roles = forms.ModelMultipleChoiceField(
queryset=WEIRole.objects, queryset=WEIRole.objects,
label=_("WEI Roles"), label=_("WEI Roles"),
@ -149,6 +154,7 @@ class WEIMembership1AForm(WEIMembershipForm):
""" """
Used to confirm registrations of first year members without choosing a bus now. Used to confirm registrations of first year members without choosing a bus now.
""" """
caution_check = None
roles = None roles = None
def clean(self): def clean(self):

View File

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

View File

@ -82,7 +82,7 @@ WORDS = {
5: "La quoi ?" 5: "La quoi ?"
}], }],
"kokarde": ["Qu'est-ce que le mot Kokarde t'évoque ?", { "kokarde": ["Qu'est-ce que le mot Kokarde t'évoque ?", {
1: "Vraiment pas mon truc les soirées", 1: "Vraiment pas mon truc les soirées...",
2: "Bof, je viens pour manger et je repars aussitôt", 2: "Bof, je viens pour manger et je repars aussitôt",
3: "Je kiffe, good vibes", 3: "Je kiffe, good vibes",
4: "Perso, je ne m'arrêterai pas de danser sur la piste !", 4: "Perso, je ne m'arrêterai pas de danser sur la piste !",
@ -117,15 +117,15 @@ WORDS = {
5: "Je pourrais en faire à n'importe qui. Pourquoi ne pas créer le club Câl[ENS] ?" 5: "Je pourrais en faire à n'importe qui. Pourquoi ne pas créer le club Câl[ENS] ?"
}], }],
"vomi": ["Quel est ton rapport au vomi ?", { "vomi": ["Quel est ton rapport au vomi ?", {
1: "C'est compliqué", 1: "C'est compliqué...",
2: "Jamais je ne vomis mais je nettoie quand mes potes vomissent", 2: "Jamais je ne vomis mais je nettoie quand mes potes vomissent",
3: "Jamais je ne vomis et jamais je ne nettoie celui de quelqu'un d'autre", 3: "Jamais je ne vomis et jamais je ne nettoie celui de quelqu'un d'autre",
4: "Je vomis quelquefois, ça arrive, faites pas cette tête, mais je fins toujours par nettoyer !", 4: "Je vomis quelquefois, ça arrive, faites pas cette tête, mais je fins toujours par nettoyer !",
5: "Je vomis à chaque soirée et ce n'est jamais moi qui nettoie" 5: "Je vomis à chaque soirée et ce n'est jamais moi qui nettoie"
}], }],
"kfet": ["Qu'est ce que la Kfet t'évoque ?", { "kfet": ["Qu'est ce que la Kfet t'évoque ?", {
1: "La Kfet, quel lieu de dépravé⋅es sérieux", 1: "La Kfet, quel lieu de dépravé⋅es sérieux...",
2: "C'est un endroit à l'hygiène plus que douteuse", 2: "C'est un endroit à l'hygiène plus que douteuse...",
3: "Téma les prix des boissons et des snacks, c'est aberrant !", 3: "Téma les prix des boissons et des snacks, c'est aberrant !",
4: "En vrai, c'est cool, petit billard, petit canapé, chill !", 4: "En vrai, c'est cool, petit billard, petit canapé, chill !",
5: "Banger, j'y reste jusqu'à la fin de mes jours" 5: "Banger, j'y reste jusqu'à la fin de mes jours"
@ -147,7 +147,7 @@ WORDS = {
"scolarite": ["Comment tu vois ton cursus à l'ENS ?", { "scolarite": ["Comment tu vois ton cursus à l'ENS ?", {
1: "La tranquillité et le travail", 1: "La tranquillité et le travail",
2: "On va s'amuser tout en bossant", 2: "On va s'amuser tout en bossant",
3: "Ça va profiter et réviser au dernier moment pour les exams", 3: "Ça va profiter et réviser au dernier moment pour les exams...",
4: "Nous festoierons sans songer aux conséquences", 4: "Nous festoierons sans songer aux conséquences",
5: "Je ne vois qu'une seule issue : la débauche" 5: "Je ne vois qu'une seule issue : la débauche"
}] }]

View File

@ -0,0 +1,372 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from functools import lru_cache
from django import forms
from django.utils.safestring import mark_safe
from django.db import transaction
from django.db.models import Q
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInformation
from ...models import WEIMembership
buses_descr = [
[
"Magi[Kar]p 🐙🎮🎲", "#ef5568", 1,
"""Vous l'aurez compris au nom du bus, l'ambiance est aux jeux et à la culture geek ! Ici, vous trouverez une ambiance
calme avec une bonne dose d'autodérision et de second degré. Que vous ayez besoin de beaucoup dormir pour tenir la soirée
du lendemain, ou que vous souhaitiez faire nuit blanche pour jouer toute la nuit, vous pouvez nous rejoindre. Votre voix
n'y survivra peut-être pas à force de chanter. PS : les meilleurs cocktails du WEI sont chez nous, à déguster, pas à
siphonner !""",
],
[
"Va[car]me 🎷🍎🔊", "#fd7a28", 3,
"""Ici, c'est le bus du bruit. Si vous voulez réveiller les autres bus en musique, apprendre de merveilleuses
mélodies au kazoo tout le week-end, ou simplement profiter d'une bonne ambiance musicale, le BDA et la
F[ENS]foire sont pour vous. Vous pourrez également goûter au célèbre cocktail de la fanfare, concocté
pour l'occasion par les tout nouveaux "meilleurs artisans v*********** de France" ! Alors que vous soyez artiste
dans l'âme ou que vous souhaitiez juste faire le plus grand Vacarme, rejoignez-nous !""",
],
[
"[Kar]aïbes 🏝️🏴‍☠️🥥", "#a5cfdd", 3,
"""Ahoy, explorateurs du WEI ! Le bus Karaibes tinvite à une traversée sous les tropiques, où lambiance est
toujours au beau fixe ! 🍹 Ici, cest soleil, rhum, et bonne humeur assurée : une atmosphère de vacances
lon se laisse porter par la chaleur humaine et la fête. Que tu sois un pirate en quête de sensations fortes ou
un amateur de chill avec un cocktail à la main, tu seras à ta place dans notre bus. Les soirées seront marquées
par des rythmes tropicaux qui te feront vibrer jusquà laube. Prêt à embarquer pour une aventure inoubliable
avec les meilleurs matelots du WEI ? On tattend sur le pont du Karaibes pour lever lancre ensemble !""",
],
[
"[Kar]di [Bus] 🎙️💅", "#e46398", 2.5,
"""Bienvenue à bord du Kardi Bus, la seul, lunique, linimitable pépite de ce weekend dintégration ! Inspiré par les
icônes suprêmes de la pop culture telles les Bratz, les Winx et autres Mean Girls, notre bus est un sanctuaire de style,
daudace et de pur plaisir. A nos cotés attends toi à siroter tes meilleurs Cosmo, sex on the Beach et autres cocktails
de maxi pétasse tout en papotant entre copains copines ! Si tu rejoins le Kardi Bus, tu entres dans un monde tu
pourras te déhancher sur du Beyoncé, Britney, Aya et autres reines de la pop ! À très vite, les futures stars du Kardi
Bus !""",
],
[
"Sparta[bus] 🐺🐒🏉", "#ebdac2", 5,
"""Dans notre bus, on vous donne un avant goût des plus grandes assos de l'ENS : les Kyottes et l'Aspique (clubs de rugby
féminin et masculin, mais pas que). Bien entendu, qui dit rugby dit les copaings, le pastaga et la Pena Bayona, mais vous
verrez par vous même qu'on est ouvert⋅e à toutes propositions quand il s'agit de faire la fête. Pour les casse-cous comme
pour les plus calmes, vous trouverez au bus Aspique-Kyottes les 2A+ qui vous feront kiffer votre WEI.""",
],
[
"Zanzo[Bus] 🤯🚸🐒", "#FFFF", 3,
"""Dans un entre-trois bien senti entre zinzinerie, enfance et vieillerie, le Zanzo[BUS] est un concentré de fun mêlé à
de la dinguerie à gogo. N'hésitez plus et rejoignez-nous pour un WEI toujours plus déjanté !""",
],
[
"Bran[Kar] 🍹🥳", "#6da1ac", 4,
"""Si vous ne connaissez pas le Bran[Kar], cest comme une grande famille qui fait un apéro, qui se bourre un peu la
gueule en discutant des heures autour dune table remplie de bouffe et de super bons cocktails (la plupart des
barmen/barwomen du bus sont les barmans de Shakens), sauf quon est un bus du Wei (vous comprendrez bien le nom de notre
bus en voyant létat de certain·e·s). Il nous arrive de faire quelques conneries, mais surtout de jouer au Bière-pong en
musique !""",
],
[
"Techno [kar]ade 🔊🚩", "#8065a3", 3,
"""Avis à tous·tes les gauchos, amoureux·ses de la fête et des manifs : le Techno [kar]ade vous ouvre grand ses bras pour
finir en beauté votre première inté. Préparez-vous à vous abreuver de cocktails (savamment élaborés) à la vibration dun
système son fabriqué pour loccasion. Des sets technos à « Mon père était tellement de gauche » en passant par « Female
Body », le car accueillant les meilleures DJs du plateau saura animer le trajet aussi bien que les soirées. Si alcool et
musique seront au rendez-vous, les maîtres mots sont sécurité et inclusivité. Qui que vous soyez et quelle que soit votre
manière de vous amuser, notre objectif est que vous vous sentiez à laise pour rencontrer au mieux les 1A, les 2A et les
(nombreuxses) 3A+ qui auront répondu à lappel. Bref, rejoignez-nous, on est super cools :)"""
],
[
"[Bus]ka-P 🥇🍻🎤", "#7c4768", 4.5,
"""Booska-p, cest le « site N°1 du Rap français ». Le [Bus]ka-p ? Le bus N°1 sur lambiance au WEI. Les nuits vont être
courtes, les cocktails vont couler à flots : tout sera réuni pour vivre un week-end dont tu te souviendras toute ta vie.
Au programme pas un seul temps mort et un maximum de rencontres pour bien commencer ta première année à lENS. Et bien
entendu, le tout accompagné des meilleurs sons, de Jul à Aya, en passant par ABBA et Sexion dAssaut. Bref, si tu veux
vivre un WEI danthologie et faire la fête, de jour comme de nuit, nous taccueillons avec plaisir !""",
],
]
def print_bus(i):
return f"""<h1 style="color:{buses_descr[i][1]};-webkit-text-stroke: 2px black;font-size: 50px;">{buses_descr[i][0]}</h1><br>
<b>Alcoolomètre : {buses_descr[i][2]} / 5 🍻</b><br><br>{buses_descr[i][3]}<br>"""
def print_all_buses():
liste = [print_bus(i) for i in range(len(buses_descr))]
return "<br><br><br><br>".join(liste)
def get_number_comment(i):
if i == 1:
return "Même pas en rêve"
elif i == 2:
return "Pas envie"
elif i == 3:
return "Mouais..."
elif i == 4:
return "Pourquoi pas !"
elif i == 5:
return "Ce bus ou rien !!!"
else:
return ""
WORDS = {
"recap":
[
"""<b>Chèr⋅e 1A, te voilà arrivé⋅e au moment fatidique du choix de ton bus !<br><br><br>
Ton bus est constitué des gens avec qui tu passeras la majorité de ton temps : que ce soit le voyage d'aller et de
retour et les différentes activité qu'ils pourront te proposer tout au long du WEI donc choisis le bien !
<br><br>Tu trouveras ci-dessous la liste de tous les bus ainsi qu'une description détaillée de ces derniers.
Prends ton temps pour étudier chacun d'eux et quand tu te sens prêt⋅e, appuie sur le bouton « J'ai pris connaissance
des bus » pour continuer
<br>(pas besoin d'apprendre par cœur chaque bus, la description de chaque bus te sera rappeler avant de lui attribuer
une note !)</b><br><br><br>""" + print_all_buses(),
{
"1": "J'ai pris connaissance des différents bus et me sent fin prêt à choisir celui qui me convient le mieux !",
}
]
}
WORDS.update({
f"bus{id}": [print_bus(id), {i: f"{get_number_comment(i)} ({i}/5)" for i in range(1, 5 + 1)}] for id in range(len(buses_descr))
})
class WEISurveyForm2024(forms.Form):
"""
Survey form for the year 2024.
Members score the different buses, from which we calculate the best associated bus.
"""
def set_registration(self, registration):
"""
Filter the bus selector with the buses of the current WEI.
"""
information = WEISurveyInformation2024(registration)
question = information.questions[information.step]
self.fields[question] = forms.ChoiceField(
label=mark_safe(WORDS[question][0]),
widget=forms.RadioSelect(),
)
answers = [(answer, WORDS[question][1][answer]) for answer in WORDS[question][1]]
self.fields[question].choices = answers
class WEIBusInformation2024(WEIBusInformation):
"""
For each question, the bus has ordered answers
"""
scores: dict
def __init__(self, bus):
self.scores = {}
for question in WORDS:
self.scores[question] = []
super().__init__(bus)
class WEISurveyInformation2024(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.
"""
step = 0
questions = list(WORDS.keys())
def __init__(self, registration):
for question in WORDS:
setattr(self, str(question), None)
super().__init__(registration)
class WEISurvey2024(WEISurvey):
"""
Survey for the year 2024.
"""
@classmethod
def get_year(cls):
return 2024
@classmethod
def get_survey_information_class(cls):
return WEISurveyInformation2024
def get_form_class(self):
return WEISurveyForm2024
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):
self.information.step += 1
for question in WORDS:
if question in form.cleaned_data:
answer = form.cleaned_data[question]
setattr(self.information, question, answer)
self.save()
@classmethod
def get_algorithm_class(cls):
return WEISurveyAlgorithm2024
def is_complete(self) -> bool:
"""
The survey is complete once the bus is chosen.
"""
for question in WORDS:
if not getattr(self.information, question):
return False
return True
@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 = 0
for question in WORDS:
s += bus_info.scores[question][str(getattr(self.information, question))]
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):
return super().clear_cache()
class WEISurveyAlgorithm2024(WEISurveyAlgorithm):
"""
The algorithm class for the year 2024.
We use Gale-Shapley algorithm to attribute 1y students into buses.
"""
@classmethod
def get_survey_class(cls):
return WEISurvey2024
@classmethod
def get_bus_information_class(cls):
return WEIBusInformation2024
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
WEISurvey2024.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

@ -0,0 +1,18 @@
# Generated by Django 4.2.15 on 2024-08-28 20:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wei', '0008_auto_20240111_1545'),
]
operations = [
migrations.AddField(
model_name='weiregistration',
name='specific_diet',
field=models.TextField(blank=True, default='', verbose_name='specific diet'),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 4.2.15 on 2024-08-29 20:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('wei', '0009_weiregistration_specific_diet'),
]
operations = [
migrations.RemoveField(
model_name='weiregistration',
name='specific_diet',
),
]

View File

@ -12,7 +12,7 @@
<div class="card-body"> <div class="card-body">
{% render_table bus_repartition_table %} {% render_table bus_repartition_table %}
<hr> <hr>
<a href="{% url 'wei:wei_bus_1A_next' pk=club.pk %}" class="btn btn-block btn-success">{% trans "Start attribution!" %}</a> <a href="{% url 'wei:wei_bus_1A_next' pk=club.pk %}" class="btn btn-block btn-success">{% trans "Start attribution !" %}</a>
<hr> <hr>
{% render_table table %} {% render_table table %}
</div> </div>

View File

@ -25,7 +25,7 @@
<dt class="col-xl-6">{% trans 'department'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'department'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.user.profile.get_department_display }}</dd> <dd class="col-xl-6">{{ object.user.profile.get_department_display }}</dd>
<dt class="col-xl-6">{% trans 'health issues'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'health issues or specific diet'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.health_issues|default:"—" }}</dd> <dd class="col-xl-6">{{ object.health_issues|default:"—" }}</dd>
<dt class="col-xl-6">{% trans 'suggested bus'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'suggested bus'|capfirst %}</dt>

View File

@ -64,7 +64,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<dt class="col-xl-6">{% trans 'birth date'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'birth date'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.birth_date }}</dd> <dd class="col-xl-6">{{ registration.birth_date }}</dd>
<dt class="col-xl-6">{% trans 'health issues'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'health issues or specific diet'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.health_issues }}</dd> <dd class="col-xl-6">{{ registration.health_issues }}</dd>
<dt class="col-xl-6">{% trans 'emergency contact name'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'emergency contact name'|capfirst %}</dt>

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.wei2023 import WEIBusInformation2023, WEISurvey2023, WORDS, WEISurveyInformation2023 from ..forms.surveys.wei2023 import WEIBusInformation2023, WEISurvey2023, WORDS, WEISurveyInformation2023
from ..models import Bus, WEIClub, WEIRegistration from ..models import Bus, WEIClub, WEIRegistration
@ -127,44 +125,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, 20 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 = WEISurvey2023(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 = WEISurvey2023(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,172 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import random
from datetime import date, timedelta
from django.contrib.auth.models import User
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 ..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.user = User.objects.create_superuser(
username="weiadmin",
password="admin",
email="admin@example.com",
)
self.user.save()
self.client.force_login(self.user)
sess = self.client.session
sess["permission_mask"] = 42
sess.save()
self.wei = WEIClub.objects.create(
name="WEI 2024",
email="wei2024@example.com",
parent_club_id=2,
membership_fee_paid=12500,
membership_fee_unpaid=5500,
membership_start='2024-01-01',
membership_end='2024-12-31',
date_start=date.today() + timedelta(days=2),
date_end='2024-12-31',
year=2024,
)
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 = WEIBusInformation2024(bus)
for question in WORDS:
information.scores[question] = {answer: random.randint(1, 5) for answer in WORDS[question][1]}
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 = WEISurveyInformation2024(registration)
for question in WORDS:
options = list(WORDS[question][1].keys())
setattr(information, question, random.choice(options))
information.step = 20
information.save(registration)
registration.save()
# Run algorithm
WEISurvey2024.get_algorithm_class()().run_algorithm()
# Ensure that everyone has its first choice
for r in WEIRegistration.objects.filter(wei=self.wei).all():
survey = WEISurvey2024(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 = WEISurveyInformation2024(registration)
for question in WORDS:
options = list(WORDS[question][1].keys())
setattr(information, question, random.choice(options))
information.step = 20
information.save(registration)
registration.save()
# Run algorithm
WEISurvey2024.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 = WEISurvey2024(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 %
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

@ -767,7 +767,7 @@ class TestDefaultWEISurvey(TestCase):
WEISurvey.update_form(None, None) WEISurvey.update_form(None, None)
self.assertEqual(CurrentSurvey.get_algorithm_class().get_survey_class(), CurrentSurvey) self.assertEqual(CurrentSurvey.get_algorithm_class().get_survey_class(), CurrentSurvey)
self.assertEqual(CurrentSurvey.get_year(), 2023) self.assertEqual(CurrentSurvey.get_year(), 2024)
class TestWeiAPI(TestAPI): class TestWeiAPI(TestAPI):

View File

@ -900,6 +900,9 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
form.fields["last_name"].initial = registration.user.last_name form.fields["last_name"].initial = registration.user.last_name
form.fields["first_name"].initial = registration.user.first_name form.fields["first_name"].initial = registration.user.first_name
if "caution_check" in form.fields:
form.fields["caution_check"].initial = registration.caution_check
if registration.soge_credit: if registration.soge_credit:
form.fields["credit_type"].disabled = True form.fields["credit_type"].disabled = True
form.fields["credit_type"].initial = NoteSpecial.objects.get(special_type="Virement bancaire") form.fields["credit_type"].initial = NoteSpecial.objects.get(special_type="Virement bancaire")
@ -941,6 +944,9 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
club = registration.wei club = registration.wei
user = registration.user user = registration.user
if "caution_check" in form.data:
registration.caution_check = form.data["caution_check"] == "on"
registration.save()
membership = form.instance membership = form.instance
membership.user = user membership.user = user
membership.club = club membership.club = club

View File

@ -32,7 +32,7 @@ Applications indispensables
* `Note <note>`_ : * `Note <note>`_ :
Les notes associées à des utilisateur⋅rices ou des clubs. Les notes associées à des utilisateur⋅rices ou des clubs.
* `Activity <activity>`_ : * `Activity <activity>`_ :
La gestion des activités (créations, gestion, entrées,) La gestion des activités (créations, gestion, entrées, ...)
* `Permission <permission>`_ : * `Permission <permission>`_ :
Backend de droits, limites les pouvoirs des utilisateur⋅rices Backend de droits, limites les pouvoirs des utilisateur⋅rices
* `API <../api>`_ : * `API <../api>`_ :
@ -64,9 +64,9 @@ Applications facultatives
* ``cas-server`` * ``cas-server``
Serveur central d'authentification, permet d'utiliser son compte de la NoteKfet2020 pour se connecter à d'autre application ayant intégrer un client. Serveur central d'authentification, permet d'utiliser son compte de la NoteKfet2020 pour se connecter à d'autre application ayant intégrer un client.
* `Scripts <https://gitlab.crans.org/bde/nk20-scripts>`_ * `Scripts <https://gitlab.crans.org/bde/nk20-scripts>`_
Ensemble de commande `./manage.py` pour la gestion de la note: import de données, verification d'intégrité, etc Ensemble de commande `./manage.py` pour la gestion de la note: import de données, verification d'intégrité, etc...
* `Treasury <treasury>`_ : * `Treasury <treasury>`_ :
Interface de gestion pour les trésorièr⋅es, émission de factures, remises de chèque, statistiques ... Interface de gestion pour les trésorièr⋅es, émission de factures, remises de chèque, statistiques...
* `WEI <wei>`_ : * `WEI <wei>`_ :
Interface de gestion du WEI. Interface de gestion du WEI.

View File

@ -43,7 +43,7 @@ l'utilisateur⋅rice, utiles pour l'adhésion au BDE :
* ``address`` : ``CharField``, adresse physique de l'utilisateur⋅rice * ``address`` : ``CharField``, adresse physique de l'utilisateur⋅rice
* ``paid`` : ``BooleanField``, indique si l'utilisateur⋅rice normalien⋅ne est rémunéré⋅e ou non (utile pour différencier les montants d'adhésion aux clubs) * ``paid`` : ``BooleanField``, indique si l'utilisateur⋅rice normalien⋅ne est rémunéré⋅e ou non (utile pour différencier les montants d'adhésion aux clubs)
* ``phone_number`` : ``CharField``, numéro de téléphone de l'utilisateur⋅rice * ``phone_number`` : ``CharField``, numéro de téléphone de l'utilisateur⋅rice
* ``section`` : ``CharField``, section de l'ENS à laquelle appartient l'utilisateur⋅rice (exemple : 1A0,) * ``section`` : ``CharField``, section de l'ENS à laquelle appartient l'utilisateur⋅rice (exemple : 1A0, ...)
Clubs Clubs
~~~~~ ~~~~~
@ -101,7 +101,7 @@ Adhésions
La Note Kfet offre la possibilité aux clubs de gérer l'adhésion de leurs membres. En plus de réguler les cotisations La Note Kfet offre la possibilité aux clubs de gérer l'adhésion de leurs membres. En plus de réguler les cotisations
des adhérent⋅es, des permissions sont octroyées sur la note en fonction des rôles au sein des clubs. Un rôle est une des adhérent⋅es, des permissions sont octroyées sur la note en fonction des rôles au sein des clubs. Un rôle est une
fonction occupée au sein d'un club (Trésorièr⋅e de club, président⋅e de club, GC Kfet, Res[pot], respo info,). fonction occupée au sein d'un club (Trésorièr⋅e de club, président⋅e de club, GC Kfet, Res[pot], respo info, ...).
Une adhésion attribue à un⋅e adhérent⋅e ses rôles. Les rôles fournissent les permissions. Par exemple, læ trésorièr⋅e d'un Une adhésion attribue à un⋅e adhérent⋅e ses rôles. Les rôles fournissent les permissions. Par exemple, læ trésorièr⋅e d'un
club a le droit de faire des transferts de et vers la note du club, tant que la source reste au-dessus de -50 €. club a le droit de faire des transferts de et vers la note du club, tant que la source reste au-dessus de -50 €.
Une adhésion est considérée comme valide si la date du jour est comprise (au sens large) entre les dates de début et Une adhésion est considérée comme valide si la date du jour est comprise (au sens large) entre les dates de début et

View File

@ -49,7 +49,7 @@ Une fois l'inscription validée, détail de ce qu'il se passe :
lui octroyant un faible nombre de permissions de base, telles que la visualisation de son compte. lui octroyant un faible nombre de permissions de base, telles que la visualisation de son compte.
* On adhère la personne au club Kfet si cela est demandé, l'adhésion commence aujourd'hui. Iel dispose d'un unique rôle : * On adhère la personne au club Kfet si cela est demandé, l'adhésion commence aujourd'hui. Iel dispose d'un unique rôle :
« Adhérent⋅e Kfet » , lui octroyant un nombre un peu plus conséquent de permissions basiques, telles que la possibilité de « Adhérent⋅e Kfet » , lui octroyant un nombre un peu plus conséquent de permissions basiques, telles que la possibilité de
faire des transactions, d'accéder aux activités, au WEI, faire des transactions, d'accéder aux activités, au WEI, ...
* Si læ nouvelleau membre a indiqué avoir ouvert un compte à la société générale, alors les transactions sont invalidées, * Si læ nouvelleau membre a indiqué avoir ouvert un compte à la société générale, alors les transactions sont invalidées,
la note n'est pas débitée (commence alors à 0 €). la note n'est pas débitée (commence alors à 0 €).

View File

@ -3744,8 +3744,8 @@ msgid "FAQ (FR)"
msgstr "FAQ (FR)" msgstr "FAQ (FR)"
#: 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 "Suche nach Attributen wie Name" msgstr "Suche nach Attributen wie Name..."
#: note_kfet/templates/base_search.html:23 #: note_kfet/templates/base_search.html:23
msgid "There is no results." msgid "There is no results."

View File

@ -3694,8 +3694,8 @@ msgid "FAQ (FR)"
msgstr "FAQ (FR)" msgstr "FAQ (FR)"
#: 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 "Buscar con atributo, como el nombre" msgstr "Buscar con atributo, como el nombre..."
#: note_kfet/templates/base_search.html:23 #: note_kfet/templates/base_search.html:23
msgid "There is no results." msgid "There is no results."

View File

@ -1930,8 +1930,8 @@ msgstr "Consommer"
#: apps/note/templates/note/conso_form.html:43 #: apps/note/templates/note/conso_form.html:43
#: apps/note/templates/note/transaction_form.html:69 #: apps/note/templates/note/transaction_form.html:69
#: apps/note/templates/note/transaction_form.html:96 #: apps/note/templates/note/transaction_form.html:96
msgid "Name or alias" msgid "Name or alias..."
msgstr "Pseudo ou alias" msgstr "Pseudo ou alias..."
#: apps/note/templates/note/conso_form.html:53 #: apps/note/templates/note/conso_form.html:53
msgid "Select consumptions" msgid "Select consumptions"
@ -2031,8 +2031,8 @@ msgid "Current price"
msgstr "Prix actuel" msgstr "Prix actuel"
#: apps/note/templates/note/transactiontemplate_list.html:13 #: apps/note/templates/note/transactiontemplate_list.html:13
msgid "Name of the button" msgid "Name of the button..."
msgstr "Nom du bouton" msgstr "Nom du bouton..."
#: apps/note/templates/note/transactiontemplate_list.html:15 #: apps/note/templates/note/transactiontemplate_list.html:15
msgid "New button" msgid "New button"
@ -3001,6 +3001,7 @@ 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:188 #: apps/wei/models.py:188
#: apps/wei/forms/registration.py:84
msgid "Caution check given" msgid "Caution check given"
msgstr "Chèque de caution donné" msgstr "Chèque de caution donné"
@ -3385,8 +3386,8 @@ msgstr ""
"coût d'adhésion." "coût d'adhésion."
#: apps/wei/templates/wei/weimembership_list.html:27 #: apps/wei/templates/wei/weimembership_list.html:27
msgid "View unvalidated registrations" msgid "View unvalidated registrations..."
msgstr "Voir les inscriptions non validées" msgstr "Voir les inscriptions non validées..."
#: apps/wei/templates/wei/weiregistration_confirm_delete.html:16 #: apps/wei/templates/wei/weiregistration_confirm_delete.html:16
msgid "This registration is already validated and can't be deleted." msgid "This registration is already validated and can't be deleted."
@ -3406,8 +3407,8 @@ msgid "There is no pre-registration found with this pattern."
msgstr "Il n'y a pas de pré-inscription en attente avec cette entrée." msgstr "Il n'y a pas de pré-inscription en attente avec cette entrée."
#: apps/wei/templates/wei/weiregistration_list.html:27 #: apps/wei/templates/wei/weiregistration_list.html:27
msgid "View validated memberships…" msgid "View validated membershipis..."
msgstr "Voir les adhésions validées" msgstr "Voir les adhésions validées..."
#: apps/wei/views.py:58 #: apps/wei/views.py:58
msgid "Search WEI" msgid "Search WEI"
@ -3672,8 +3673,8 @@ msgid "FAQ (FR)"
msgstr "FAQ (FR)" msgstr "FAQ (FR)"
#: 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..."
#: note_kfet/templates/base_search.html:23 #: note_kfet/templates/base_search.html:23
msgid "There is no results." msgid "There is no results."

View File

@ -265,11 +265,9 @@ OAUTH2_PROVIDER = {
'SCOPES_BACKEND_CLASS': 'permission.scopes.PermissionScopes', 'SCOPES_BACKEND_CLASS': 'permission.scopes.PermissionScopes',
'OAUTH2_VALIDATOR_CLASS': "permission.scopes.PermissionOAuth2Validator", 'OAUTH2_VALIDATOR_CLASS': "permission.scopes.PermissionOAuth2Validator",
'REFRESH_TOKEN_EXPIRE_SECONDS': timedelta(days=14), 'REFRESH_TOKEN_EXPIRE_SECONDS': timedelta(days=14),
'PKCE_REQUIRED': False, # PKCE (fix a breaking change of django-oauth-toolkit 2.0.0)
} }
# PKCE (fix a breaking change of django-oauth-toolkit 2.0.0)
PKCE_REQUIRED = False
# Take control on how widget templates are sourced # Take control on how widget templates are sourced
# See https://docs.djangoproject.com/en/2.2/ref/forms/renderers/#templatessetting # See https://docs.djangoproject.com/en/2.2/ref/forms/renderers/#templatessetting
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'

View File

@ -12,7 +12,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
</h3> </h3>
<div class="card-body"> <div class="card-body">
<input id="searchbar" type="text" class="form-control" <input id="searchbar" type="text" class="form-control"
placeholder="{% trans "Search by attribute such as name" %}"> placeholder="{% trans "Search by attribute such as name..." %}">
</div> </div>
<div id="dynamic-table"> <div id="dynamic-table">
{% if table.data %} {% if table.data %}
@ -75,4 +75,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
init_table(); init_table();
}); });
</script> </script>
{% endblock %} {% endblock %}