1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-09-25 19:08:18 +02:00

Compare commits

..

26 Commits

Author SHA1 Message Date
ehouarn
d17ab26f2f Merge branch 'phone_input' into 'main'
Phone input

See merge request bde/nk20!351
2025-09-03 18:40:26 +02:00
ehouarn
297f289d7e Merge branch 'wei' into 'main'
New informative questions

See merge request bde/nk20!350
2025-08-31 22:25:57 +02:00
Ehouarn
034ad9a4ce tests 2025-08-31 22:04:45 +02:00
Ehouarn
897d37f74d New informative questions 2025-08-31 21:45:09 +02:00
sable
42fb0aa2d6 Merge branch 'translations' into 'main'
minor translate

See merge request bde/nk20!349
2025-08-31 13:36:58 +02:00
sable
4bc43ec3cb minor translate 2025-08-31 13:19:27 +02:00
ehouarn
00737da69f Merge branch 'family' into 'main'
minor fixe

See merge request bde/nk20!348
2025-08-31 13:02:29 +02:00
sable
6eb192b823 minor fixe 2025-08-31 12:43:37 +02:00
Ehouarn
0934b8fa34 Patch 2025-08-30 16:15:55 +02:00
ehouarn
bcd6444ff2 Merge branch 'family' into 'main'
INSTALLED_APPS checks

See merge request bde/nk20!347
2025-08-30 02:12:51 +02:00
Ehouarn
2a638e7b32 INSTALLED_APPS checks 2025-08-30 01:55:03 +02:00
Ehouarn
7633c9ab4b Better phone input (no invalid number) 2025-08-29 18:36:18 +02:00
ehouarn
bb06206a9b Merge branch 'wei' into 'main'
Answers to survey

See merge request bde/nk20!346
2025-08-29 17:31:13 +02:00
Ehouarn
55be3c9836 Answers to survey 2025-08-29 17:13:52 +02:00
ehouarn
2ac19ab7be Merge branch 'translations' into 'main'
Translations

See merge request bde/nk20!345
2025-08-29 14:44:17 +02:00
ehouarn
7d359dec13 Update django.po 2025-08-29 14:12:25 +02:00
ehouarn
1015a5dba1 Merge branch 'main' into 'translations'
Main

See merge request bde/nk20!344
2025-08-29 14:08:58 +02:00
ehouarn
8f9f650826 Merge branch 'family' into 'main'
Family

See merge request bde/nk20!343
2025-08-28 12:03:09 +02:00
ehouarn
99a90867cc Merge branch 'main' into 'family'
# Conflicts:
#   locale/fr/LC_MESSAGES/django.po
2025-08-28 11:23:22 +02:00
Ehouarn
0d69695b00 Last commit 2025-08-28 11:19:45 +02:00
ehouarn
92f6d11cb5 Merge branch 'translations' into 'main'
Some WEI translations

See merge request bde/nk20!342
2025-08-21 00:03:43 +02:00
Ehouarn
1fdb30d7d2 Some WEI translations 2025-08-20 23:37:34 +02:00
ehouarn
6975ed6df6 Merge branch 'wei' into 'main'
Survey questions

See merge request bde/nk20!341
2025-08-20 23:30:34 +02:00
Ehouarn
4da87872bd Survey questions 2025-08-20 22:59:37 +02:00
Ehouarn
c25f6ca2c1 Corrected test 2025-08-14 00:34:39 +02:00
Ehouarn
4d567cdcc7 Achievement unicity && management pop-up behaviour 2025-08-13 23:57:05 +02:00
22 changed files with 576 additions and 258 deletions

View File

@@ -75,13 +75,24 @@ class BatchAchievementsAPIView(APIView):
challenge_ids = request.data.get('challenges')
families = Family.objects.filter(id__in=family_ids)
challenges = Challenge.objects.filter(id__in=challenge_ids)
results = []
for family in families:
for challenge in challenges:
a = Achievement(family=family, challenge=challenge)
a.save(update_score=False)
a, created = Achievement.objects.get_or_create(family=family, challenge=challenge)
if created:
results.append({
'family': family.name,
'challenge': challenge.name,
'status': 'created'
})
else:
results.append({
'family': family.name,
'challenge': challenge.name,
'status': 'existed',
})
for family in families:
family.update_score()
Family.update_ranking()
return Response({'status': 'ok'}, status=status.HTTP_201_CREATED)
return Response({'results': results}, status=status.HTTP_201_CREATED)

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.2.4 on 2025-08-13 20:26
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('family', '0004_remove_challenge_obtained'),
]
operations = [
migrations.AlterUniqueTogether(
name='achievement',
unique_together={('challenge', 'family')},
),
]

View File

@@ -170,6 +170,7 @@ class Achievement(models.Model):
)
class Meta:
unique_together = ('challenge', 'family',)
verbose_name = _('achievement')
verbose_name_plural = _('achievements')

View File

@@ -25,11 +25,6 @@ $(document).ready(function () {
location.hash = this.getAttribute('href')
})
// Ensure we begin in single consumption. Fix issue with TurboLinks and BootstrapJS
document.getElementById("consume_all").addEventListener('click', consumeAll)
})
notes = []
@@ -140,13 +135,25 @@ function consumeAll () {
headers: {
'X-CSRFToken': CSRF_TOKEN
},
success: function () {
success: function (data) {
reset()
addMsg("Défis validés pour les familles!", 'success', 5000)
},
error: function (e) {
reset()
addMsg("Erreur lors de la création des achievements.",'danger',5000)
data.results.forEach(function (result) {
if (result.status === 'created') {
addMsg(
interpolate(gettext('Invalid achievement for challenge %s ' +
'and family %s created.'), [result.challenge, result.family]),
'success',
5000
)
} else {
addMsg(
interpolate(gettext('An achievement for challenge %s ' +
'and family %s already exists.'), [result.challenge, result.family]),
'danger',
8000
)
}
})
}
})
}

View File

@@ -177,25 +177,27 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content border-success">
<div class="modal-header bg-success text-white">
<h5 class="modal-title" id="validationModalLabel">{% trans "Challenge validated" %}</h5>
<h5 class="modal-title" id="validationModalLabel">{% trans "Confirmation" %}</h5>
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p><strong>Are you sure you want to validate this challenge?</strong></p>
<p>To have your challenge officially validated, please send a message with:</p>
<ul>
<li>The name of the family</li>
<li>The name of the challenge</li>
<li>A photo or video as proof</li>
</ul>
<p>
<strong>Send it via WhatsApp to:
{% for num in phone_numbers %}
<a href="https://wa.me/{{ num }}" target="_blank">{{ num }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
</p>
<p><strong>{% trans "Are you sure you want to validate this challenge?" %}</strong></p>
<p>{% trans "To have your challenge officially validated, please send a message with:" %}</p>
<ul>
<li>{% trans "The name of the family" %}</li>
<li>{% trans "The name of the challenge" %}</li>
<li>{% trans "A photo or video as proof" %}</li>
</ul>
<p>
<strong>{% trans "Send it via WhatsApp to:" %}</strong>
{% if phone_numbers %}"
{% for num in phone_numbers %}
<a href="https://wa.me/{{ num }}" target="_blank">{{ num }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
{% endif %}
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">{% trans "OK" %}</button>
@@ -227,6 +229,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
document.getElementById("consume_all").addEventListener("click", function () {
$('#validationModal').modal('show');
});
$('#validationModal .btn-primary').on('click', function () {
consumeAll();
});
{% if user_family %}
document.getElementById("select_my_family").addEventListener("click", function () {

View File

@@ -239,6 +239,12 @@ class TestBatchAchievements(APITestCase):
Challenge.objects.create(name=f'Challenge {i}', description='', points=50) for i in range(3)
]
self.achievement = Achievement.objects.create(
family=self.families[0],
challenge=self.challenges[0],
valid=False,
)
self.url = reverse("family:api:batch_achievements")
def test_batch_achievement_creation(self):
@@ -254,7 +260,11 @@ class TestBatchAchievements(APITestCase):
)
self.assertEqual(response.status_code, 201)
self.assertEqual(response.data['status'], 'ok')
for result in response.data['results']:
if result['family'] == self.families[0].name and result['challenge'] == self.challenges[0].name:
self.assertEqual(result['status'], 'existed')
else:
self.assertEqual(result['status'], 'created')
expected_count = len(family_ids) * len(challenge_ids)
self.assertEqual(Achievement.objects.count(), expected_count)

View File

@@ -10,6 +10,7 @@ from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.models import User
from django.db import transaction
from django.forms import CheckboxSelectMultiple
from phonenumber_field.formfields import PhoneNumberField
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial, Alias
@@ -45,6 +46,11 @@ class ProfileForm(forms.ModelForm):
A form for the extras field provided by the :model:`member.Profile` model.
"""
# Remove widget=forms.HiddenInput() if you want to use report frequency.
phone_number = PhoneNumberField(
widget=forms.TextInput(attrs={"type": "tel", "class": "form-control"}),
required=False
)
report_frequency = forms.IntegerField(required=False, initial=0, label=_("Report frequency"))
last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date"))
@@ -72,7 +78,12 @@ class ProfileForm(forms.ModelForm):
if not self.instance.section or (("department" in self.changed_data
or "promotion" in self.changed_data) and "section" not in self.changed_data):
self.instance.section = self.instance.section_generated
return super().save(commit)
instance = super().save(commit=False)
if instance.phone_number:
instance.phone_number = instance.phone_number.as_e164
if commit:
instance.save()
return instance
class Meta:
model = Profile

View File

@@ -1,6 +1,8 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
def save_user_profile(instance, created, raw, **_kwargs):
"""
@@ -16,7 +18,7 @@ def save_user_profile(instance, created, raw, **_kwargs):
def update_wei_registration_fee_on_membership_creation(sender, instance, created, **kwargs):
if not hasattr(instance, "_no_signal") and created:
if not hasattr(instance, "_no_signal") and 'wei' in settings.INSTALLED_APPS and created:
from wei.models import WEIRegistration
if instance.club.id == 1 or instance.club.id == 2:
registrations = WEIRegistration.objects.filter(
@@ -29,8 +31,8 @@ def update_wei_registration_fee_on_membership_creation(sender, instance, created
def update_wei_registration_fee_on_club_change(sender, instance, **kwargs):
from wei.models import WEIRegistration
if not hasattr(instance, "_no_signal") and (instance.id == 1 or instance.id == 2):
if not hasattr(instance, "_no_signal") and 'wei' in settings.INSTALLED_APPS and (instance.id == 1 or instance.id == 2):
from wei.models import WEIRegistration
registrations = WEIRegistration.objects.filter(
wei__year=instance.membership_start.year,
)

View File

@@ -7,6 +7,7 @@
<dt class="col-xl-6">{% trans 'username'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.username }}</dd>
{% if family_app_installed %}
<dt class="col-xl-6">{% trans 'family'|capfirst %}</dt>
<dd class="col-xl-6">
{% if families %}
@@ -17,6 +18,7 @@
<span class="text-muted">Aucune</span>
{% endif %}
</dd>
{% endif %}
{% if user_object.pk == user.pk %}
<dt class="col-xl-6">{% trans 'password'|capfirst %}</dt>

View File

@@ -10,7 +10,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{{ title }}
</h3>
<div class="card-body">
<form method="post">
<form method="post" id="profile-form">
{% csrf_token %}
{{ form | crispy }}
{{ profile_form | crispy }}
@@ -20,4 +20,46 @@ SPDX-License-Identifier: GPL-3.0-or-later
</form>
</div>
</div>
{% endblock %}
{% block extrajavascript %}
<!-- intl-tel-input CSS/JS -->
<script>
(() => {
const input = document.querySelector("input[name='phone_number']");
const form = document.querySelector("#profile-form");
if (!input || !form) {
console.error("Input phone_number ou form introuvable.");
}
const iti = window.intlTelInput(input, {
initialCountry: "auto",
nationalMode: false,
autoPlaceholder: "off",
geoIpLookup: callback => {
fetch("https://ipapi.co/json")
.then(res => res.json())
.then(data => callback(data.country_code))
.catch(() => callback("fr"));
},
loadUtils: () => import("https://cdn.jsdelivr.net/npm/intl-tel-input@25.5.2/build/js/utils.js"),
});
form.addEventListener("submit", function(e){
if (!input.value.trim()) {
return;
}
const number = iti.getNumber(intlTelInput.utils.numberFormat.E164);
if (number) {
input.value = number;
form.submit();
} else {
e.preventDefault();
input.focus();
}
});
})();
</script>
{% endblock %}

View File

@@ -207,9 +207,10 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
modified_note.is_active = True
context["can_unlock_note"] = not user.note.is_active and PermissionBackend\
.check_perm(self.request, "note.change_noteuser_is_active", modified_note)
families = Family.objects.filter(memberships__user=user).distinct()
context["families"] = families
if 'family' in settings.INSTALLED_APPS:
context["family_app_installed"] = True
families = Family.objects.filter(memberships__user=user).distinct()
context["families"] = families
return context

View File

@@ -10,145 +10,225 @@ from django import forms
from django.db import transaction
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from django.utils.safestring import mark_safe
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInformation
from ...models import WEIMembership, Bus
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',
'Fiesta', 'Graillance', 'Move it move it', 'Calme', 'Nerd et geek', 'Jeux de rôles et danse rock',
'Strass et paillettes', 'Spectaculaire', 'Splendide', 'Flow inégalable', 'Rap', 'Battles légendaires',
'Techno', 'Alcool', 'Kiffeur·euse', 'Rugby', 'Médiéval', 'Festif',
'Stylé', 'Chipie', 'Rétro', 'Vache', 'Farfadet', 'Fanfare',
],
'questions': {
'Question 1': [
'Description 1',
"alcool": [
"""Sur une échelle allant de 0 (= 0 alcool ou très peu) à 5 (= la fontaine de jouvence alcoolique),
quel niveau de consommation dalcool souhaiterais-tu ?""",
{
3: 'Réponse 1 Madagas[car]',
4: 'Réponse 1 Y2[KAR]',
2: 'Réponse 1 Tcherno[bus]',
5: 'Réponse 1 [Kar]tier',
1: 'Réponse 1 [Car]cassonne',
6: 'Réponse 1 O[car]ina',
7: 'Réponse 1 Show[bus]',
8: 'Réponse 1 [Car]ioca'
42: 4,
47: 1,
48: 3,
45: 3.5,
44: 4,
46: 5,
43: 3,
49: 3
}
],
'Question 2': [
'Description 2',
"voie_post_bac": [
"""Si la DA du bus de ton choix correspondait à une voie post-bac, laquelle serait-elle ?""",
{
3: 'Réponse 2 Madagas[car]',
4: 'Réponse 2 Y2[KAR]',
2: 'Réponse 2 Tcherno[bus]',
5: 'Réponse 2 [Kar]tier',
1: 'Réponse 2 [Car]cassonne',
6: 'Réponse 2 O[car]ina',
7: 'Réponse 2 Show[bus]',
8: 'Réponse 2 [Car]ioca'
42: "Double licence cuisine/arts du cirque option burger",
47: "BTS Exploration de donjon",
48: "Ecole des stars en herbe",
45: "Déscolarisation précoce",
44: "Rattrapage pour excès de kiff",
46: "Double cursus STAPS / Licence dhistoire",
43: "Recherche active dun sugar daddy/dun sugar mommy",
49: "Licence de musicologie"
}
],
'Question 3': [
'Description 3',
"boite": [
"""Tu es seul·e sur une île déserte et devant toi il y a une sombre boîte de taille raisonnable.
Quy a-t-il à lintérieur ?""",
{
3: 'Réponse 3 Madagas[car]',
4: 'Réponse 3 Y2[KAR]',
2: 'Réponse 3 Tcherno[bus]',
5: 'Réponse 3 [Kar]tier',
1: 'Réponse 3 [Car]cassonne',
6: 'Réponse 3 O[car]ina',
7: 'Réponse 3 Show[bus]',
8: 'Réponse 3 [Car]ioca'
42: "Un burgouzz de valouzz",
47: "Un ocarina (pour me téléporter hors de ce bourbier)",
48: "Des paillettes, un micro de karaoké et une enceinte bluetooth",
45: "Un kebab",
44: "Une 86 et un caisson pour taper du pied",
46: "Une épée, un ballon et une tireuse",
43: "Des lunettes de soleil",
49: "Mon instrument de musique"
}
],
'Question 4': [
'Description 4',
"tardif": [
"""Il est 00h, tu as passé la journée à la plage avec tes copains et iels te proposent de prolonger parce
quaprès tout, il ny a plus personne sur la plage à cette heure-ci. Tu nhabites pas loin mais tenchaînes
demain avec une journée similaire avec un autre groupe damis parce que tes trop #busy. Que fais-tu ?""",
{
3: 'Réponse 4 Madagas[car]',
4: 'Réponse 4 Y2[KAR]',
2: 'Réponse 4 Tcherno[bus]',
5: 'Réponse 4 [Kar]tier',
1: 'Réponse 4 [Car]cassonne',
6: 'Réponse 4 O[car]ina',
7: 'Réponse 4 Show[bus]',
8: 'Réponse 4 [Car]ioca'
42: "On veut se déchaîner toute la nuit !!",
47: "Je prends une glace et chill un moment avant daller dormir",
48: "Jenfile mes boogie shoes pour enflammer le dancefloor avec elleux et lancer un concours de slay, le perdant finit la bouteille de rhum",
45: "La fête continuuuuue",
44: "Soirée sangria plage → boîte → lever de soleil sur la plage",
46: "Minuit ? Cest lheure du genepi. On commence les alcools forts !!",
43: "Tenchaînes direct (faut pas les priver de ta présence)",
49: "On continue en mode chill (soirée potins)"
}
],
'Question 5': [
'Description 5',
"cohesion": [
"""Cest la rentrée de Seconde et tu découvres ta classe, tes camarades et ta prof principale!!!
qui vous propose une activité de cohésion. Laquelle est-elle ?""",
{
3: 'Réponse 5 Madagas[car]',
4: 'Réponse 5 Y2[KAR]',
2: 'Réponse 5 Tcherno[bus]',
5: 'Réponse 5 [Kar]tier',
1: 'Réponse 5 [Car]cassonne',
6: 'Réponse 5 O[car]ina',
7: 'Réponse 5 Show[bus]',
8: 'Réponse 5 [Car]ioca'
42: "Un relais cubi en ventriglisse",
47: "Un jeu de rôle",
48: "Organiser la soirée de lannée dans le lycée. Le thème : SLAY (Spotlight, Love, Amaze/All-night, Yeah), paillettes, disco",
45: "La prof de français propose un slam parce qu'elle pense que c'est du rap littéraire qui fera plaisir aux élèves",
44: "Ptit escape game + apéro",
46: "Joute avec des boucliers Gilbert",
43: "Tournage dun clip de confessions nocturnes de Diams",
49: "Je sais pas jai raté mon BAFA"
}
],
'Question 6': [
'Description 6',
"artiste": [
"""Cest lété et la saison des festivals a commencé. Tu regardes la programmation du festival
pas loin de chez toi et tu découvres avec joie la présence dun·e artiste. De qui sagit-il ?""",
{
3: 'Réponse 6 Madagas[car]',
4: 'Réponse 6 Y2[KAR]',
2: 'Réponse 6 Tcherno[bus]',
5: 'Réponse 6 [Kar]tier',
1: 'Réponse 6 [Car]cassonne',
6: 'Réponse 6 O[car]ina',
7: 'Réponse 6 Show[bus]',
8: 'Réponse 6 [Car]ioca'
42: "Moto-Moto (il chantera son fameux tube “je les aime grosses, je les aime bombées”)",
47: "Hatsune Miku",
48: "Rihanna",
45: "Vald",
44: "Qui connaît vraiment les noms des artistes de tech ?",
46: "Perceval",
43: "Fatal bazooka",
49: "Måneskin"
}
],
'Question 7': [
'Description 7',
"annonce_noel": [
"""Cest Noël et tu revois toute ta famille, oncles, tantes, cousin·e·s, grands-parents, la totale.
Dun coup, tu te lèves, tapotes de manière pompeuse sur ton verre avec un de tes couverts.
Quannonces-tu ?""",
{
3: 'Réponse 7 Madagas[car]',
4: 'Réponse 7 Y2[KAR]',
2: 'Réponse 7 Tcherno[bus]',
5: 'Réponse 7 [Kar]tier',
1: 'Réponse 7 [Car]cassonne',
6: 'Réponse 7 O[car]ina',
7: 'Réponse 7 Show[bus]',
8: 'Réponse 7 [Car]ioca'
42: """« Chère famille. Je sais bien que nous avions dit : pas de politique à table.
Je ne peux toutefois me retenir de vous annoncer une grande nouvelle…
jai décidé de quitter la ville pour consacrer ma vie au culte du Roi Julian.
A moi la jungle luxuriante, là où le soleil chaud caresse les palmiers,
où les lémuriens dansent avec frénésie et où chaque repas est une ode au burger sauvage.
Longue vie à Sa Majesté le Roi Julian ! »""",
47: "« Jai perdu »",
48: "« Mes chers parents je pars, jarrête lENS pour devenir DJ slay à Ibiza »",
45: "Jinterromps le repas pour rapper les 6min de bande organisée",
44: "« Digestif ? Pétanque ? Les deux ? »",
46: "« Montjoie St Denis à bas la Macronie »",
43: "« Je suis enceinte » (cest faux jai juste besoin dattention)",
49: """Discours de remerciement :
je lance un powerpoint de 65 slides et sors une feuille A4 blanche (je fais semblant de lire mon discours dessus)"""
}
],
'Question 8': [
'Description 8',
"vacances": [
"""Les vacances sont là et taimerais bien partir quelque part, mais où ?""",
{
3: 'Réponse 8 Madagas[car]',
4: 'Réponse 8 Y2[KAR]',
2: 'Réponse 8 Tcherno[bus]',
5: 'Réponse 8 [Kar]tier',
1: 'Réponse 8 [Car]cassonne',
6: 'Réponse 8 O[car]ina',
7: 'Réponse 8 Show[bus]',
8: 'Réponse 8 [Car]ioca'
42: "A Madagascar, à bord dun bus conduit par des pingouins",
47: "Dans ma chambre",
48: "Rio de Janeiro",
45: "N'importe où tant qu'on peut sortir tous les soirs",
44: "Tu suis les plans du club ski ou de piratens",
46: "Carcassonne",
43: "Coachella",
49: "Dans les montagnes de la république populaire dAuvergne-Rhônes-Alpes pour profiter de la fraîcheur, de la nature et de mes ami·e·s"
}
],
'Question 9': [
'Description 9',
"loisir": [
"""Tas fini ta journée de cours et tu tapprêtes à profiter dune activité/hobby/loisir de ton choix.
Laquelle est-ce ?""",
{
3: 'Réponse 9 Madagas[car]',
4: 'Réponse 9 Y2[KAR]',
2: 'Réponse 9 Tcherno[bus]',
5: 'Réponse 9 [Kar]tier',
1: 'Réponse 9 [Car]cassonne',
6: 'Réponse 9 O[car]ina',
7: 'Réponse 9 Show[bus]',
8: 'Réponse 9 [Car]ioca'
42: "Cueillir des noix de coco",
47: "Essayer de travailler puis chill avec des potes autour dun jeu en buvant du thé",
48: "Repet du nouveau spectacle de mon club, before (potins) puis sortie avec les potes jusquau bout de la night",
45: "Zoner avec les copaings jusquà pas dheure",
44: "Go Kfet pour se faire traquenard jusquà 3h du mat",
46: "Déterminer ce qui est le plus solide entre mon crâne et une ecocup",
43: "Revoir pour la 6e fois gossip girl au fond de ton lit",
49: "Jouer de mon instrument préféré avec les copains/copines pour préparer le prochain concert #solidays"
}
],
"plan": [
"""Tu reçois un message sur la conversation de groupe que tu partages avec tes potes :
vous êtes chaud·e·s pour vous retrouver. Quel plan tattire le plus ?""",
{
42: """Après-midi piscine, puis before arrosé de mojito,
avant daller séclater en pot avec toute la savane et de finir sur un after spécial pina colada""",
47: """(matin) : Ptit jeu de rôle
(repas) : le traditionnel poké-tacos
(juste après le repas) : combat avec des épées en mousse avec les copains!
(16h00) : pause thé
(fin daprès midi) : initiation à la danse rock
(soirée) : découverte dun jeu de société avec des règles obscures
""",
48: "Soirée champagne and chic : spectacle et dîner au moulin rouge puis soirée sur les champs",
45: "Se regrouper pour une soirée, même si il nest encore que 10h",
44: "Ptit poké qui termine en koin koin avec after poker",
46: "Une dégustation de bière, un rugby et toute autre activité joviale",
43: "Un brunch de pour papoter puis friperies",
49: "Soirée raclette !"
}
]
},
'stats': [
{
"question": """Le WEI est structuré par bus, et au sein de chaque bus, par équipes.
Pour toi, être dans une équipe où tout le monde reste sobre (primo-entrants comme encadrants) c'est :""",
"answers": [
(1, "Inenvisageable"),
(2, "À contre cœur"),
(3, "Pourquoi pas"),
(4, "Souhaitable"),
(5, "Nécessaire"),
],
"help_text": "(De toute façon aucun alcool n'est consommé pendant les trajets du bus, ni aller, ni retour.)",
},
{
"question": "Faire partie d'un bus qui n'apporte pas de boisson alcoolisée pour ses membres, pour toi c'est :",
"answers": [
(1, "Inenvisageable"),
(2, "À contre cœur"),
(3, "Pourquoi pas"),
(4, "Souhaitable"),
(5, "Nécessaire"),
],
"help_text": """(Tout les bus apportent de l'alcool cette année, cette question sert à l'organisation pour l'année prochaine.
De plus il y aura de toute façon de l'alcool commun au WEI et aucun alcool n'est consommé pendant les trajets en bus.)""",
},
]
}
IMAGES = {
"vacances": {
49: "/static/wei/img/logo_auvergne_rhone_alpes.jpg",
}
}
NB_WORDS = 5
class OptionalImageRadioSelect(forms.RadioSelect):
def __init__(self, images=None, *args, **kwargs):
self.images = images or {}
super().__init__(*args, **kwargs)
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
option = super().create_option(name, value, label, selected, index, subindex=subindex, attrs=attrs)
img_url = self.images.get(value)
if img_url:
option['label'] = mark_safe(f'{label} <img src="{img_url}" style="height:32px;vertical-align:middle;">')
else:
option['label'] = label
return option
class WEISurveyForm2025(forms.Form):
"""
Survey form for the year 2025.
@@ -170,7 +250,7 @@ class WEISurveyForm2025(forms.Form):
if information.step == 0:
self.fields["words"] = forms.MultipleChoiceField(
label=_(f"Choose {NB_WORDS} words:"),
label=_(f"Select {NB_WORDS} words that describe the WEI experience you want to have."),
choices=[(w, w) for w in WORDS['list']],
widget=forms.CheckboxSelectMultiple(),
required=True,
@@ -178,38 +258,34 @@ class WEISurveyForm2025(forms.Form):
if self.is_valid():
return
buses = WEISurveyAlgorithm2025.get_buses()
informations = {bus: WEIBusInformation2025(bus) for bus in buses}
scores = sum((list(informations[bus].scores.values()) for bus in buses), [])
if scores:
average_score = sum(scores) / len(scores)
else:
average_score = 0
preferred_words = {
bus: [word for word in WORDS['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)
all_preferred_words = WORDS['list']
rng.shuffle(all_preferred_words)
self.fields["words"].choices = [(w, w) for w in all_preferred_words]
else:
elif information.step <= len(WORDS['questions']):
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)
if q == 'alcool':
choices = [(i / 2, str(i / 2)) for i in range(11)]
else:
choices = [(k, v) for k, v in answers.items()]
rng.shuffle(choices)
self.fields[q] = forms.ChoiceField(
label=desc,
choices=choices,
widget=forms.RadioSelect,
widget=OptionalImageRadioSelect(images=IMAGES.get(q, {})),
required=True,
)
elif information.step == len(WORDS['questions']) + 1:
for i, v in enumerate(WORDS['stats']):
self.fields[f'stat_{i}'] = forms.ChoiceField(
label=v['question'],
choices=v['answers'],
widget=forms.RadioSelect(),
required=False,
help_text=_(v.get('help_text', ''))
)
def clean_words(self):
data = self.cleaned_data['words']
@@ -226,8 +302,6 @@ class WEIBusInformation2025(WEIBusInformation):
def __init__(self, bus):
self.scores = {}
for word in WORDS['list']:
self.scores[word] = 0
super().__init__(bus)
@@ -235,7 +309,9 @@ class BusInformationForm2025(forms.ModelForm):
class Meta:
model = Bus
fields = ['information_json']
widgets = {}
widgets = {
'information_json': forms.HiddenInput(),
}
def __init__(self, *args, words=None, **kwargs):
super().__init__(*args, **kwargs)
@@ -257,7 +333,7 @@ class BusInformationForm2025(forms.ModelForm):
label=word,
choices=choices,
coerce=int,
initial=initial_scores.get(word, 0),
initial=initial_scores.get(word, 0) if word in initial_scores else None,
required=True,
widget=forms.RadioSelect,
help_text=_("Rate between 0 and 5."),
@@ -285,7 +361,7 @@ class WEISurveyInformation2025(WEISurveyInformation):
step = 0
def __init__(self, registration):
for i in range(1, 5):
for i in range(1, NB_WORDS + 1):
setattr(self, "word" + str(i), None)
for q in WORDS['questions']:
setattr(self, q, None)
@@ -297,7 +373,7 @@ class WEISurveyInformation2025(WEISurveyInformation):
"""
self.step = 0
self.seed = 0
for i in range(1, 5):
for i in range(1, NB_WORDS + 1):
setattr(self, f"word{i}", None)
for q in WORDS['questions']:
setattr(self, q, None)
@@ -336,7 +412,7 @@ class WEISurvey2025(WEISurvey):
setattr(self.information, "word" + str(i), word)
self.information.step += 1
self.save()
else:
elif 1 <= self.information.step <= len(WORDS['questions']):
questions = list(WORDS['questions'].keys())
idx = self.information.step - 1
if idx < len(questions):
@@ -344,6 +420,13 @@ class WEISurvey2025(WEISurvey):
setattr(self.information, q, form.cleaned_data[q])
self.information.step += 1
self.save()
else:
for i, __ in enumerate(WORDS['stats']):
ans = form.cleaned_data.get(f'stat_{i}')
if ans is not None:
setattr(self.information, f'stat_{i}', ans)
self.information.step += 1
self.save()
@classmethod
def get_algorithm_class(cls):
@@ -353,7 +436,7 @@ class WEISurvey2025(WEISurvey):
"""
The survey is complete once the bus is chosen.
"""
return self.information.step > len(WORDS['questions'])
return self.information.step > len(WORDS['questions']) + 1
@classmethod
@lru_cache()
@@ -371,8 +454,9 @@ class WEISurvey2025(WEISurvey):
"""
if not self.is_complete():
raise ValueError("Survey is not ended, can't calculate score")
# Score is the given score by the bus subtracted to the mid-score of the buses.
s = sum(1 for q in WORDS['questions'] if getattr(self.information, q) == bus.pk)
s = sum(1 for q in WORDS['questions'] if q != 'alcool' and getattr(self.information, q) == bus.pk)
if 'alcool' in WORDS['questions'] and bus.pk in WORDS['questions']['alcool'][1] and hasattr(self.information, 'alcool'):
s -= abs(float(self.information.alcool) - float(WORDS['questions']['alcool'][1][bus.pk]))
return s
@lru_cache()
@@ -396,7 +480,7 @@ class WEISurvey2025(WEISurvey):
@lru_cache()
def ordered_buses(self):
"""
Force the choice of bus to be in the 3 preferred buses according to the words
Order the buses by the score_questions of the survey.
"""
values = list(self.scores_per_bus().items())
values.sort(key=lambda item: -item[1][0])
@@ -513,7 +597,7 @@ class WEISurveyAlgorithm2025(WEISurveyAlgorithm):
for survey2 in surveys:
if not survey2.information.valid or survey2.information.get_selected_bus() != bus:
continue
score2 = survey2.score_questions(bus)
score2 = survey2.score_words(bus)
if current_scores[1] <= score2: # Ignore better students
continue
if least_preferred_survey is None or score2 < least_score:

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -22,8 +22,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endif %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus' pk=object.pk %}"
data-turbolinks="false">{% trans "Edit" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus_info' pk=object.pk %}"
data-turbolinks="false">{% trans "Edit information" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus_info' pk=object.pk %}"
data-turbolinks="false">{% trans "Edit information for survey" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_team' pk=object.pk %}"
data-turbolinks="false">{% trans "Add team" %}</a>
</div>

View File

@@ -31,15 +31,24 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a class="btn btn-success" href="{% url "wei:wei_register_1A_myself" wei_pk=club.pk %}" data-turbolinks="false">
{% trans "Register to the WEI! 1A" %}
</a>
{% endif %}
<a class="btn btn-success" href="{% url "wei:wei_register_2A_myself" wei_pk=club.pk %}" data-turbolinks="false">
{% trans "Register to the WEI! 2A+" %}</a>
{% else %}
<a class="btn btn-success" href="{% url "wei:wei_register_2A_myself" wei_pk=club.pk %}" data-turbolinks="false">
{% trans "Register to the WEI! 2A+" %}
</a>
{% endif %}
{% else %}
{% if registration.validated %}
<a class="btn btn-warning" href="{% url "wei:wei_update_registration" pk=my_registration.pk %}"
data-turbolinks="false">
{% trans "Update my registration" %}
</a>
{% if not not_first_year %}
{% endif %}
{% if my_registration.first_year %}
{% if not survey_complete %}
<a class="btn btn-warning" href="{% url "wei:wei_survey" pk=my_registration.pk %}" data-turbolinks="false">
{% trans "Continue survey" %}
</a>
{% endif %}
<a class="btn btn-warning" href="{% url "wei:wei_survey" pk=my_registration.pk %}?reset=true" data-turbolinks="false">
{% trans "Restart survey" %}
</a>

View File

@@ -11,7 +11,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{{ title }}
</h3>
<div class="card-body">
<form method="post">
<form id="registration-form" method="post">
{% csrf_token %}
{{ form|crispy }}
{{ membership_form|crispy }}
@@ -22,6 +22,46 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endblock %}
{% block extrajavascript %}
<!-- intl-tel-input CSS/JS -->
<script>
(() => {
const input = document.querySelector("input[name='emergency_contact_phone']");
const form = document.querySelector("#registration-form");
if (!input || !form) {
console.error("Input phone_number ou form introuvable.");
}
const iti = window.intlTelInput(input, {
initialCountry: "auto",
nationalMode: false,
autoPlaceholder: "off",
geoIpLookup: callback => {
fetch("https://ipapi.co/json")
.then(res => res.json())
.then(data => callback(data.country_code))
.catch(() => callback("fr"));
},
loadUtils: () => import("https://cdn.jsdelivr.net/npm/intl-tel-input@25.5.2/build/js/utils.js"),
});
form.addEventListener("submit", function(e){
if (!input.value.trim()) {
return;
}
const number = iti.getNumber(intlTelInput.utils.numberFormat.E164);
if (number) {
input.value = number;
form.submit();
} else {
e.preventDefault();
input.focus();
}
});
})();
</script>
{% if not object.membership %}
<script>
$(document).ready(function () {

View File

@@ -53,9 +53,11 @@ class TestWEIAlgorithm(TestCase):
birth_date='2000-01-01',
)
information = WEISurveyInformation2025(registration)
for j in range(1, 21):
for j in range(1, 1 + NB_WORDS):
setattr(information, f'word{j}', random.choice(WORDS['list']))
information.step = 20
for q in WORDS['questions']:
setattr(information, q, random.choice(list(WORDS['questions'][q][1].keys())))
information.step = len(WORDS['questions']) + 2
information.save(registration)
registration.save()
@@ -87,7 +89,7 @@ class TestWEIAlgorithm(TestCase):
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.step = len(WORDS['questions']) + 2
information.save(registration)
registration.save()
survey = WEISurvey2025(registration)
@@ -105,8 +107,6 @@ class TestWEIAlgorithm(TestCase):
survey = WEISurvey2025(r)
chosen_bus = survey.information.get_selected_bus()
buses = survey.ordered_buses()
'''print(buses)
print(chosen_bus)'''
self.assertIn(chosen_bus, [x[0] for x in buses])
score_questions, score_words = next(scores for bus, scores in buses if bus == chosen_bus)
max_score_questions = max(buses[i][1][0] for i in range(len(buses)))

View File

@@ -166,6 +166,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, D
my_registration = WEIRegistration.objects.filter(wei=club, user=self.request.user)
if my_registration.exists():
my_registration = my_registration.get()
context["survey_complete"] = CurrentSurvey(my_registration).is_complete()
else:
my_registration = None
context["my_registration"] = my_registration
@@ -213,6 +214,8 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, D
context["not_first_year"] = WEIMembership.objects.filter(user=self.request.user).exists()
context["registration_validated"] = WEIMembership.objects.filter(registration=my_registration).exists() if my_registration else False
qs = WEIMembership.objects.filter(club=club, registration__first_year=True, bus__isnull=True)
context["can_validate_1a"] = PermissionBackend.check_perm(
self.request, "wei.change_weimembership_bus", qs.first()) if qs.exists() else False
@@ -1231,7 +1234,6 @@ class WEIUpdateMembershipView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateVi
return form
def get_success_url(self):
print("get_success_url")
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.registration.wei.pk})

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-08-13 16:21+0200\n"
"POT-Creation-Date: 2025-08-20 23:34+0200\n"
"PO-Revision-Date: 2022-04-11 22:05+0200\n"
"Last-Translator: ehouarn <ehouarn@crans.org>\n"
"Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\n"
@@ -589,7 +589,6 @@ msgstr "adhésions"
#: apps/family/models.py:108
#, python-brace-format
#| msgid "Membership of {user} for the club {club}"
msgid "Family membership of {user} to {family}"
msgstr "Adhésion de {user} à la famille {family}"
@@ -610,15 +609,15 @@ msgstr "défis"
msgid "obtained at"
msgstr "réalisé le"
#: apps/family/models.py:173
#: apps/family/models.py:174
msgid "achievement"
msgstr "succès"
#: apps/family/models.py:174
#: apps/family/models.py:175
msgid "achievements"
msgstr "succès"
#: apps/family/models.py:177
#: apps/family/models.py:178
#, python-brace-format
msgid "Challenge {challenge} carried out by Family {family}"
msgstr "Défi {challenge} réalisé par la famille {family}"
@@ -655,15 +654,12 @@ msgid "Delete achievement"
msgstr "Supprimer le succès"
#: apps/family/templates/family/achievement_confirm_delete.html:14
#, fuzzy
#| msgid ""
#| "Are you sure you want to delete this invoice? This action can't be undone."
msgid ""
"Are you sure you want to delete this achievement? This action can't be "
"undone."
msgstr ""
"Êtes-vous sûr⋅e de vouloir supprimer cette facture ? Cette action ne pourra "
"pas être annulée."
"Êtes-vous sûr⋅e de vouloir supprimer ce succès ? Cette action ne pourra pas "
"être annulée."
#: apps/family/templates/family/achievement_confirm_delete.html:20
#: apps/family/templates/family/achievement_confirm_validate.html:20
@@ -675,15 +671,12 @@ msgid "Validate achievement"
msgstr "Valider le succès"
#: apps/family/templates/family/achievement_confirm_validate.html:14
#, fuzzy
#| msgid ""
#| "Are you sure you want to delete this invoice? This action can't be undone."
msgid ""
"Are you sure you want to validate this achievement? This action can't be "
"undone."
msgstr ""
"Êtes-vous sûr⋅e de vouloir supprimer cette facture ? Cette action ne pourra "
"pas être annulée."
"Êtes-vous sûr⋅e de vouloir valider ce succès ? Cette action ne pourra pas "
"être annulée."
#: apps/family/templates/family/achievement_list.html:13
msgid "Invalid achievements history"
@@ -777,7 +770,7 @@ msgstr "Créer une famille ou un défi"
#: apps/family/templates/family/manage.html:96
msgid "Add a family"
msgstr "Ajouter une famille"
msgstr "Fonder une famille"
#: apps/family/templates/family/manage.html:101
msgid "Add a challenge"
@@ -801,10 +794,36 @@ msgid "Recent achievements history"
msgstr "Historique des derniers succès"
#: apps/family/templates/family/manage.html:180
msgid "Challenge validated"
msgstr "Défi validé"
msgid "Confirmation"
msgstr "Confirmation"
#: apps/family/templates/family/manage.html:201
#: apps/family/templates/family/manage.html:186
msgid "Are you sure you want to validate this challenge?"
msgstr "Êtes-vous sûr⋅e de vouloir valider ce défi ?"
#: apps/family/templates/family/manage.html:187
msgid ""
"To have your challenge officially validated, please send a message with:"
msgstr ""
"Pour que le défi soit officiellement validé, envoyez un message contenant :"
#: apps/family/templates/family/manage.html:189
msgid "The name of the family"
msgstr "Le nom de la famille"
#: apps/family/templates/family/manage.html:190
msgid "The name of the challenge"
msgstr "Le nom du défi"
#: apps/family/templates/family/manage.html:191
msgid "A photo or video as proof"
msgstr "Une preuve photo ou vidéo"
#: apps/family/templates/family/manage.html:194
msgid "Send it via WhatsApp to:"
msgstr "Envoyez le via WhasApp au :"
#: apps/family/templates/family/manage.html:202
msgid "OK"
msgstr "OK"
@@ -1734,7 +1753,7 @@ msgstr "Membres du club"
#: apps/member/templates/member/club_detail.html:40
#: apps/member/templates/member/profile_detail.html:32
#: apps/wei/templates/wei/weiclub_detail.html:98
#: apps/wei/templates/wei/weiclub_detail.html:105
msgid "Transaction history"
msgstr "Historique des transactions"
@@ -2276,8 +2295,8 @@ msgstr ""
"mode de paiement et un⋅e utilisateur⋅rice ou un club"
#: apps/note/models/transactions.py:357 apps/note/models/transactions.py:360
#: apps/note/models/transactions.py:363 apps/wei/views.py:1134
#: apps/wei/views.py:1138
#: apps/note/models/transactions.py:363 apps/wei/views.py:1135
#: apps/wei/views.py:1139
msgid "This field is required."
msgstr "Ce champ est requis."
@@ -3345,17 +3364,18 @@ msgstr "Cette équipe n'appartient pas à ce bus."
msgid "Choose a word:"
msgstr "Choisissez un mot :"
#: apps/wei/forms/surveys/wei2025.py:173
#: apps/wei/forms/surveys/wei2025.py:211
#, python-brace-format
msgid "Choose {NB_WORDS} words:"
msgstr "Choisissez {NB_WORDS} mots :"
msgid ""
"Select {NB_WORDS} words that describe the WEI experience you want to have."
msgstr "Sélectionne {NB_WORDS} mots qui décrivent lexpérience WEI que tu souhaites vivre."
#: apps/wei/forms/surveys/wei2025.py:217
#: apps/wei/forms/surveys/wei2025.py:242
#, python-brace-format
msgid "Please choose exactly {NB_WORDS} words"
msgstr ""
msgstr "Choisis exactement {NB_WORDS} mots"
#: apps/wei/forms/surveys/wei2025.py:263
#: apps/wei/forms/surveys/wei2025.py:288
msgid "Rate between 0 and 5."
msgstr "Note entre 0 et 5."
@@ -3390,7 +3410,7 @@ msgid "Information about the survey for new members, encoded in JSON"
msgstr ""
"Informations sur le sondage pour les nouveaux membres, encodées en JSON"
#: apps/wei/models.py:108 apps/wei/templates/wei/weiclub_detail.html:56
#: apps/wei/models.py:108 apps/wei/templates/wei/weiclub_detail.html:63
msgid "Buses"
msgstr "Bus"
@@ -3628,11 +3648,11 @@ msgstr "Prix du WEI (étudiant⋅es)"
msgid "WEI list"
msgstr "Liste des WEI"
#: apps/wei/templates/wei/base.html:79 apps/wei/views.py:584
#: apps/wei/templates/wei/base.html:79 apps/wei/views.py:585
msgid "Register 1A"
msgstr "Inscrire un⋅e 1A"
#: apps/wei/templates/wei/base.html:83 apps/wei/views.py:680
#: apps/wei/templates/wei/base.html:83 apps/wei/views.py:681
msgid "Register 2A+"
msgstr "Inscrire un⋅e 2A+"
@@ -3649,8 +3669,8 @@ msgid "View club"
msgstr "Voir le club"
#: apps/wei/templates/wei/bus_detail.html:26
msgid "Edit information"
msgstr "Modifier les informations"
msgid "Edit information for survey"
msgstr "Modifier les informations du sondage"
#: apps/wei/templates/wei/bus_detail.html:28
#: apps/wei/templates/wei/busteam_detail.html:24
@@ -3699,23 +3719,27 @@ msgstr "M'inscrire au WEI ! 1A"
msgid "Register to the WEI! 2A+"
msgstr "M'inscrire au WEI ! 2A+"
#: apps/wei/templates/wei/weiclub_detail.html:40
#: apps/wei/templates/wei/weiclub_detail.html:42
msgid "Update my registration"
msgstr "Modifier mon inscription"
#: apps/wei/templates/wei/weiclub_detail.html:44
#: apps/wei/templates/wei/weiclub_detail.html:47
msgid "Continue survey"
msgstr "Continuer le questionnaire"
#: apps/wei/templates/wei/weiclub_detail.html:51
msgid "Restart survey"
msgstr "Recommencer le questionnaire"
#: apps/wei/templates/wei/weiclub_detail.html:68
#: apps/wei/templates/wei/weiclub_detail.html:75
msgid "Members of the WEI"
msgstr "Membres du WEI"
#: apps/wei/templates/wei/weiclub_detail.html:80
#: apps/wei/templates/wei/weiclub_detail.html:87
msgid "Unvalidated registrations"
msgstr "Inscriptions non validées"
#: apps/wei/templates/wei/weiclub_detail.html:90
#: apps/wei/templates/wei/weiclub_detail.html:97
msgid "Attribute buses"
msgstr "Répartition dans les bus"
@@ -3751,7 +3775,7 @@ msgstr "Informations brutes du sondage"
msgid "The algorithm didn't run."
msgstr "L'algorithme n'a pas été exécuté."
#: apps/wei/templates/wei/weimembership_form.html:98 apps/wei/views.py:1028
#: apps/wei/templates/wei/weimembership_form.html:98 apps/wei/views.py:1029
msgid "Deposit check given"
msgstr "Chèque de caution donné"
@@ -3877,63 +3901,63 @@ msgstr "Chercher un WEI"
msgid "WEI Detail"
msgstr "Détails du WEI"
#: apps/wei/views.py:229
#: apps/wei/views.py:230
msgid "View members of the WEI"
msgstr "Voir les membres du WEI"
#: apps/wei/views.py:262
#: apps/wei/views.py:263
msgid "Find WEI Membership"
msgstr "Trouver une adhésion au WEI"
#: apps/wei/views.py:272
#: apps/wei/views.py:273
msgid "View registrations to the WEI"
msgstr "Voir les inscriptions au WEI"
#: apps/wei/views.py:318
#: apps/wei/views.py:319
msgid "Find WEI Registration"
msgstr "Trouver une inscription au WEI"
#: apps/wei/views.py:329
#: apps/wei/views.py:330
msgid "Update the WEI"
msgstr "Modifier le WEI"
#: apps/wei/views.py:350
#: apps/wei/views.py:351
msgid "Create new bus"
msgstr "Ajouter un nouveau bus"
#: apps/wei/views.py:388
#: apps/wei/views.py:389
msgid "Update bus"
msgstr "Modifier le bus"
#: apps/wei/views.py:420
#: apps/wei/views.py:421
msgid "Manage bus"
msgstr "Gérer le bus"
#: apps/wei/views.py:447
#: apps/wei/views.py:448
msgid "Create new team"
msgstr "Créer une nouvelle équipe"
#: apps/wei/views.py:491
#: apps/wei/views.py:492
msgid "Update team"
msgstr "Modifier l'équipe"
#: apps/wei/views.py:526
#: apps/wei/views.py:527
msgid "Manage WEI team"
msgstr "Gérer l'équipe WEI"
#: apps/wei/views.py:548
#: apps/wei/views.py:549
msgid "Register first year student to the WEI"
msgstr "Inscrire un⋅e 1A au WEI"
#: apps/wei/views.py:605 apps/wei/views.py:698
#: apps/wei/views.py:606 apps/wei/views.py:699
msgid "Check if you will open a Société Générale account"
msgstr "Cochez cette case si vous ouvrez un compte à la Société Générale."
#: apps/wei/views.py:616 apps/wei/views.py:728
#: apps/wei/views.py:617 apps/wei/views.py:729
msgid "This user is already registered to this WEI."
msgstr "Cette personne est déjà inscrite au WEI."
#: apps/wei/views.py:621
#: apps/wei/views.py:622
msgid ""
"This user can't be in her/his first year since he/she has already "
"participated to a WEI."
@@ -3941,67 +3965,67 @@ msgstr ""
"Cet⋅te utilisateur⋅rice ne peut pas être en première année puisqu'iel a déjà "
"participé à un WEI."
#: apps/wei/views.py:644
#: apps/wei/views.py:645
msgid "Register old student to the WEI"
msgstr "Inscrire un⋅e 2A+ au WEI"
#: apps/wei/views.py:702 apps/wei/views.py:825
#: apps/wei/views.py:703 apps/wei/views.py:826
msgid "You already opened an account in the Société générale."
msgstr "Vous avez déjà ouvert un compte auprès de la société générale."
#: apps/wei/views.py:715 apps/wei/views.py:821
#: apps/wei/views.py:716 apps/wei/views.py:822
msgid "Choose how you want to pay the deposit"
msgstr "Choisissez comment payer la caution"
#: apps/wei/views.py:767
#: apps/wei/views.py:768
msgid "Update WEI Registration"
msgstr "Modifier l'inscription WEI"
#: apps/wei/views.py:811
#: apps/wei/views.py:812
msgid "Tick if the deposit check has been given"
msgstr "Cochez si le chèque de caution a été donné"
#: apps/wei/views.py:850
#: apps/wei/views.py:851
msgid "No membership found for this registration"
msgstr "Pas d'adhésion trouvée pour cette inscription"
#: apps/wei/views.py:859
#: apps/wei/views.py:860
msgid "You don't have the permission to update memberships"
msgstr "Vous n'avez pas la permission de modifier une inscription"
#: apps/wei/views.py:865
#: apps/wei/views.py:866
#, python-format
msgid "You don't have the permission to update the field %(field)s"
msgstr "Vous n'avez pas la permission de modifier le champ %(field)s"
#: apps/wei/views.py:906
#: apps/wei/views.py:907
msgid "Delete WEI registration"
msgstr "Supprimer l'inscription WEI"
#: apps/wei/views.py:917
#: apps/wei/views.py:918
msgid "You don't have the right to delete this WEI registration."
msgstr "Vous n'avez pas la permission de supprimer cette inscription au WEI."
#: apps/wei/views.py:935
#: apps/wei/views.py:936
msgid "Validate WEI registration"
msgstr "Valider l'inscription WEI"
#: apps/wei/views.py:1029
#: apps/wei/views.py:1030
msgid "Only treasurers can validate this field"
msgstr "Seul·e·s les trésorier·ère·s peuvent valider ce champ"
#: apps/wei/views.py:1035
#: apps/wei/views.py:1036
msgid "Create deposit transaction"
msgstr "Créer une transaction de caution"
#: apps/wei/views.py:1036
#: apps/wei/views.py:1037
#, python-format
msgid ""
"A transaction of %(amount).2f€ will be created from the user's Note account"
msgstr ""
"Un transaction de %(amount).2f€ va être créée depuis la note de l'utilisateur"
#: apps/wei/views.py:1124
#: apps/wei/views.py:1125
#, python-format
msgid ""
"This user doesn't have enough money to join this club and pay the deposit. "
@@ -4011,12 +4035,12 @@ msgstr ""
"payer la caution. Solde actuel : %(balance)d€, crédit : %(credit)d€, "
"requis : %(needed)d€"
#: apps/wei/views.py:1177
#: apps/wei/views.py:1178
#, python-format
msgid "Deposit %(name)s"
msgstr "Caution %(name)s"
#: apps/wei/views.py:1202
#: apps/wei/views.py:1203
msgid "Update WEI Membership"
msgstr "Modifier une adhésion au WEI"
@@ -4641,6 +4665,9 @@ msgstr ""
"d'adhésion. Vous devez également valider votre adresse email en suivant le "
"lien que vous avez reçu."
#~ msgid "Challenge validated"
#~ msgstr "Défi validé"
#~ msgid "Deposit amount"
#~ msgstr "Caution"

View File

@@ -306,8 +306,8 @@ PIC_WIDTH = 200
PIC_RATIO = 1
# Custom phone number format
PHONENUMBER_DB_FORMAT = 'NATIONAL'
PHONENUMBER_DEFAULT_REGION = 'FR'
PHONENUMBER_DB_FORMAT = 'E164'
PHONENUMBER_DEFAULT_REGION = None
# We add custom information to CAS, in order to give a normalized name to other services
CAS_AUTH_CLASS = 'member.auth.CustomAuthUser'

View File

@@ -29,6 +29,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
<link rel="stylesheet" href="{% static "bootstrap4/css/bootstrap.min.css" %}">
<link rel="stylesheet" href="{% static "font-awesome/css/font-awesome.min.css" %}">
<link rel="stylesheet" href="{% static "css/custom.css" %}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/intl-tel-input@25.5.2/build/css/intlTelInput.css">
{# JQuery, Bootstrap and Turbolinks JavaScript #}
<script src="{% static "jquery/jquery.min.js" %}"></script>
@@ -41,6 +43,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
{# Translation in javascript files #}
<script src="{% static "js/jsi18n/"|add:LANGUAGE_CODE|add:".js" %}"></script>
<script src="https://cdn.jsdelivr.net/npm/intl-tel-input@25.5.2/build/js/intlTelInput.min.js"></script>
{# If extra ressources are needed for a form, load here #}
{% if form.media %}
{{ form.media }}
@@ -82,7 +86,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if user.is_authenticated %}
<li class="nav-item">
{% url 'family:family_list' as url %}
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-users"></i> {% trans 'Families' %}</a>
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-home"></i> {% trans 'Families' %}</a>
</li>
{% endif %}

View File

@@ -19,7 +19,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endblocktrans %}
</div>
<form method="post">
<form method="post" id="profile_form">
{% csrf_token %}
{{ form|crispy }}
{{ profile_form|crispy }}
@@ -31,3 +31,45 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div>
</div>
{% endblock %}
{% block extrajavascript %}
<!-- intl-tel-input CSS/JS -->
<script>
(() => {
const input = document.querySelector("input[name='phone_number']");
const form = document.querySelector("#profile_form");
if (!input || !form) {
console.error("Input phone_number ou form introuvable.");
}
const iti = window.intlTelInput(input, {
initialCountry: "auto",
nationalMode: false,
autoPlaceholder: "off",
geoIpLookup: callback => {
fetch("https://ipapi.co/json")
.then(res => res.json())
.then(data => callback(data.country_code))
.catch(() => callback("fr"));
},
loadUtils: () => import("https://cdn.jsdelivr.net/npm/intl-tel-input@25.5.2/build/js/utils.js"),
});
form.addEventListener("submit", function(e){
if (!input.value.trim()) {
return;
}
const number = iti.getNumber(intlTelInput.utils.numberFormat.E164);
if (number) {
input.value = number;
form.submit();
} else {
e.preventDefault();
input.focus();
}
});
})();
</script>
{% endblock %}