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

Compare commits

...

13 Commits

Author SHA1 Message Date
ehouarn
cb3b34f874 Merge branch 'small_features' into 'main'
Small features

See merge request bde/nk20!352
2025-09-27 13:39:34 +02:00
Ehouarn
0962a3735e Better Food search 2025-09-27 13:19:48 +02:00
Ehouarn
9907cfbd86 Autocomplete Credit reason with 'Rechargement note' 2025-09-27 01:17:33 +02:00
Ehouarn
ad90887691 Search activities 2025-09-26 22:58:30 +02:00
Ehouarn
47d2476b51 Allow to view activity entries on Activity tab 2025-09-25 00:08:56 +02:00
Ehouarn
5d8720cf46 Phone input without permission fixed 2025-09-24 22:22:23 +02:00
Ehouarn
8700144dea Permissions 2025-09-24 21:48:56 +02:00
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
Ehouarn
0934b8fa34 Patch 2025-08-30 16:15:55 +02:00
Ehouarn
7633c9ab4b Better phone input (no invalid number) 2025-08-29 18:36:18 +02:00
19 changed files with 348 additions and 31 deletions

View File

@@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "base_search.html" %}
{% comment %} {% comment %}
SPDX-License-Identifier: GPL-3.0-or-later SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %} {% endcomment %}
@@ -44,6 +44,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
<h3 class="card-header text-center"> <h3 class="card-header text-center">
{% trans "All activities" %} {% trans "All activities" %}
</h3> </h3>
{% render_table table %} {% render_table all %}
</div> </div>
{{ block.super }}
{% endblock %} {% endblock %}

View File

@@ -1,7 +1,7 @@
{% comment %} {% comment %}
SPDX-License-Identifier: GPL-3.0-or-later SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %} {% endcomment %}
{% load i18n perms pretty_money %} {% load i18n perms pretty_money dict_get %}
{% url 'activity:activity_detail' activity.pk as activity_detail_url %} {% url 'activity:activity_detail' activity.pk as activity_detail_url %}
<div id="activity_info" class="card bg-light shadow mb-3"> <div id="activity_info" class="card bg-light shadow mb-3">
@@ -53,6 +53,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
<dt class="col-xl-6">{% trans 'opened'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'opened'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.open|yesno }}</dd> <dd class="col-xl-6">{{ activity.open|yesno }}</dd>
</dl> </dl>
{% if show_entries|dict_get:activity %}
<h2 class="text-center">
{{ entries_count|dict_get:activity }}
{% if entries_count|dict_get:activity >= 2 %}{% trans "entries" %}{% else %}{% trans "entry" %}{% endif %}
</h2>
{% endif %}
</div> </div>
<div class="card-footer text-center"> <div class="card-footer text-center">

View File

View File

@@ -0,0 +1,12 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django import template
def dict_get(d, key):
return d.get(key)
register = template.Library()
register.filter('dict_get', dict_get)

View File

@@ -67,32 +67,65 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin
tables = [ tables = [
lambda data: ActivityTable(data, prefix="all-"), lambda data: ActivityTable(data, prefix="all-"),
lambda data: ActivityTable(data, prefix="upcoming-"), lambda data: ActivityTable(data, prefix="upcoming-"),
lambda data: ActivityTable(data, prefix="search-"),
] ]
extra_context = {"title": _("Activities")} extra_context = {"title": _("Activities")}
def get_queryset(self, **kwargs): def get_queryset(self, **kwargs):
return super().get_queryset(**kwargs).distinct() """
Filter the user list with the given pattern.
"""
return super().get_queryset().distinct()
def get_tables_data(self): def get_tables_data(self):
# first table = all activities, second table = upcoming # first table = all activities, second table = upcoming, third table = search
# table search
qs = self.get_queryset().order_by('-date_start')
if "search" in self.request.GET and self.request.GET['search']:
pattern = self.request.GET['search']
# check regex
valid_regex = is_regex(pattern)
suffix = '__iregex' if valid_regex else '__istartswith'
prefix = '^' if valid_regex else ''
qs = qs.filter(Q(**{f'name{suffix}': prefix + pattern})
| Q(**{f'organizer__name{suffix}': prefix + pattern})
| Q(**{f'organizer__note__alias__name{suffix}': prefix + pattern}))
else:
qs = qs.none()
search_table = qs.filter(PermissionBackend.filter_queryset(self.request, Activity, 'view'))
return [ return [
self.get_queryset().order_by("-date_start"), self.get_queryset().order_by("-date_start"),
Activity.objects.filter(date_end__gt=timezone.now()) Activity.objects.filter(date_end__gt=timezone.now())
.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")) .filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))
.distinct() .distinct()
.order_by("date_start") .order_by("date_start"),
search_table,
] ]
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
tables = context["tables"] tables = context["tables"]
for name, table in zip(["table", "upcoming"], tables): for name, table in zip(["all", "upcoming", "table"], tables):
context[name] = table context[name] = table
started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all() started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
context["started_activities"] = started_activities context["started_activities"] = started_activities
entries_count = {}
show_entries = {}
for activity in started_activities:
if activity.activity_type.manage_entries:
entries = Entry.objects.filter(activity=activity)
entries_count[activity] = entries.count()
show_entries[activity] = True
context["entries_count"] = entries_count
context["show_entries"] = show_entries
return context return context
@@ -137,6 +170,14 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMix
"placeholder": "" "placeholder": ""
} }
} }
if self.object.activity_type.manage_entries:
entries = Entry.objects.filter(activity=self.object)
context["entries_count"] = {self.object: entries.count()}
context["show_entries"] = {self.object: timezone.now() > timezone.localtime(self.object.date_start)}
else:
context["entries_count"] = {self.object: 0}
context["show_entries"] = {self.object: False}
return context return context

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import django_tables2 as tables import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
from .models import Food from .models import Food
@@ -10,10 +11,25 @@ class FoodTable(tables.Table):
""" """
List all foods. List all foods.
""" """
qr_code_numbers = tables.Column(empty_values=(), verbose_name=_("QR Codes"), orderable=False)
date = tables.Column(empty_values=(), verbose_name=_("Arrival/creation date"), orderable=False)
def render_date(self, record):
if record.__class__.__name__ == "BasicFood":
return record.arrival_date.strftime("%d/%m/%Y %H:%M")
elif record.__class__.__name__ == "TransformedFood":
return record.creation_date.strftime("%d/%m/%Y %H:%M")
else:
return "--"
def render_qr_code_numbers(self, record):
return ", ".join(str(q.qr_code_number) for q in record.QR_code.all())
class Meta: class Meta:
model = Food model = Food
template_name = 'django_tables2/bootstrap4.html' template_name = 'django_tables2/bootstrap4.html'
fields = ('name', 'owner', 'allergens', 'expiry_date') fields = ('name', 'owner', 'qr_code_numbers', 'allergens', 'date', 'expiry_date')
row_attrs = { row_attrs = {
'class': 'table-row', 'class': 'table-row',
'data-href': lambda record: 'detail/' + str(record.pk), 'data-href': lambda record: 'detail/' + str(record.pk),

View File

@@ -34,6 +34,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="form-check">
<label for="stock_only" class="form-check-label">
<input id="stock_only" name="stock_only" type="checkbox" class="checkboxinput form-check-input" checked>
{% trans "Filter with only food in stock" %}
</label>
</div>
<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>
@@ -114,7 +120,26 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endif %} {% endif %}
</div> </div>
<script type="text/javascript">
let old_pattern = null;
let searchbar_obj = $("#searchbar");
let stock_only_obj = $("#stock_only");
function reloadTable() {
let pattern = searchbar_obj.val();
$("#dynamic-table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + (
stock_only_obj.is(':checked') ? "" : "&stock=1") + " #dynamic-table");
}
searchbar_obj.keyup(reloadTable);
stock_only_obj.change(reloadTable);
$(document).on("click", ".table-row", function () {
window.document.location = $(this).data("href");
});
</script>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
document.getElementById('goButton').addEventListener('click', function(event) { document.getElementById('goButton').addEventListener('click', function(event) {

View File

@@ -65,9 +65,13 @@ class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, Li
suffix = '__iregex' if valid_regex else '__istartswith' suffix = '__iregex' if valid_regex else '__istartswith'
prefix = '^' if valid_regex else '' prefix = '^' if valid_regex else ''
qs = qs.filter(Q(**{f'name{suffix}': prefix + pattern}) qs = qs.filter(Q(**{f'name{suffix}': prefix + pattern})
| Q(**{f'owner__name{suffix}': prefix + pattern})) | Q(**{f'owner__name{suffix}': prefix + pattern})
| Q(**{f'owner__note__alias__name{suffix}': prefix + pattern}))
else: else:
qs = qs.none() qs = qs.none()
if "stock" not in self.request.GET or not self.request.GET["stock"] == '1':
qs = qs.filter(end_of_life='')
search_table = qs.filter(PermissionBackend.filter_queryset(self.request, Food, 'view')) search_table = qs.filter(PermissionBackend.filter_queryset(self.request, Food, 'view'))
# table open # table open
open_table = self.get_queryset().order_by('expiry_date').filter( open_table = self.get_queryset().order_by('expiry_date').filter(
@@ -95,6 +99,7 @@ class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, Li
owner=club, end_of_life='').filter( owner=club, end_of_life='').filter(
PermissionBackend.filter_queryset(self.request, Food, 'view') PermissionBackend.filter_queryset(self.request, Food, 'view')
)) ))
return [search_table, open_table, served_table] + club_table return [search_table, open_table, served_table] + club_table
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@@ -218,7 +223,7 @@ class BasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
copy = self.request.GET.get('copy', None) copy = self.request.GET.get('copy', None)
if copy is not None: if copy is not None:
food = BasicFood.objects.get(pk=copy) food = BasicFood.objects.get(pk=copy)
print(context['form'].fields)
for field in context['form'].fields: for field in context['form'].fields:
if field == 'allergens': if field == 'allergens':
context['form'].fields[field].initial = getattr(food, field).all() context['form'].fields[field].initial = getattr(food, field).all()

View File

@@ -10,6 +10,7 @@ from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import transaction from django.db import transaction
from django.forms import CheckboxSelectMultiple from django.forms import CheckboxSelectMultiple
from phonenumber_field.formfields import PhoneNumberField
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial, Alias 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. A form for the extras field provided by the :model:`member.Profile` model.
""" """
# Remove widget=forms.HiddenInput() if you want to use report frequency. # 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")) report_frequency = forms.IntegerField(required=False, initial=0, label=_("Report frequency"))
last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date")) 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 if not self.instance.section or (("department" in self.changed_data
or "promotion" in self.changed_data) and "section" not in self.changed_data): or "promotion" in self.changed_data) and "section" not in self.changed_data):
self.instance.section = self.instance.section_generated 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: class Meta:
model = Profile model = Profile

View File

@@ -10,7 +10,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{{ title }} {{ title }}
</h3> </h3>
<div class="card-body"> <div class="card-body">
<form method="post"> <form method="post" id="profile-form">
{% csrf_token %} {% csrf_token %}
{{ form | crispy }} {{ form | crispy }}
{{ profile_form | crispy }} {{ profile_form | crispy }}
@@ -21,3 +21,45 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
</div> </div>
{% endblock %} {% 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 || input.type === "hidden" || input.disabled || input.readOnly) {
return;
}
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

@@ -67,6 +67,8 @@ $(document).ready(function () {
last.quantity = 1 last.quantity = 1
if (last.note.club) { if (last.note.club) {
$('#last_name').val(last.note.name) $('#last_name').val(last.note.name)
$('#first_name').val(last.note.name) $('#first_name').val(last.note.name)
@@ -111,6 +113,7 @@ $(document).ready(function () {
dest.removeClass('d-none') dest.removeClass('d-none')
$('#dest_note_list').removeClass('d-none') $('#dest_note_list').removeClass('d-none')
$('#debit_type').addClass('d-none') $('#debit_type').addClass('d-none')
$('#reason').val('')
$('#source_note_label').text(select_emitters_label) $('#source_note_label').text(select_emitters_label)
$('#dest_note_label').text(select_receveirs_label) $('#dest_note_label').text(select_receveirs_label)
@@ -134,6 +137,7 @@ $(document).ready(function () {
dest.val('') dest.val('')
dest.tooltip('hide') dest.tooltip('hide')
$('#debit_type').addClass('d-none') $('#debit_type').addClass('d-none')
$('#reason').val('Rechargement note')
$('#source_note_label').text(transfer_type_label) $('#source_note_label').text(transfer_type_label)
$('#dest_note_label').text(select_receveir_label) $('#dest_note_label').text(select_receveir_label)
@@ -162,6 +166,7 @@ $(document).ready(function () {
dest.addClass('d-none') dest.addClass('d-none')
dest.tooltip('hide') dest.tooltip('hide')
$('#debit_type').removeClass('d-none') $('#debit_type').removeClass('d-none')
$('#reason').val('')
$('#source_note_label').text(select_emitter_label) $('#source_note_label').text(select_emitter_label)
$('#dest_note_label').text(transfer_type_label) $('#dest_note_label').text(transfer_type_label)

View File

@@ -4430,6 +4430,22 @@
"description": "Modifier le type de caution de mon inscription WEI tant qu'elle n'est pas validée" "description": "Modifier le type de caution de mon inscription WEI tant qu'elle n'est pas validée"
} }
}, },
{
"model": "permission.permission",
"pk": 298,
"fields": {
"model": [
"wei",
"bus"
],
"query": "{\"pk\": [\"membership\", \"weimembership\", \"bus\", \"pk\"], \"wei__date_end__gte\": [\"today\"]}",
"type": "change",
"mask": 2,
"field": "information_json",
"permanent": false,
"description": "Modifier les informations du bus"
}
},
{ {
"model": "permission.permission", "model": "permission.permission",
"pk": 311, "pk": 311,
@@ -4833,7 +4849,10 @@
221, 221,
247, 247,
258, 258,
259 259,
260,
263,
265
] ]
} }
}, },
@@ -4845,7 +4864,6 @@
"name": "Pr\u00e9sident\u22c5e de club", "name": "Pr\u00e9sident\u22c5e de club",
"permissions": [ "permissions": [
62, 62,
135,
142 142
] ]
} }
@@ -5122,7 +5140,8 @@
289, 289,
290, 290,
291, 291,
293 293,
298
] ]
} }
}, },
@@ -5233,7 +5252,9 @@
168, 168,
176, 176,
177, 177,
197 197,
311,
319
] ]
} }
}, },
@@ -5313,7 +5334,8 @@
289, 289,
290, 290,
291, 291,
293 293,
298
] ]
} }
}, },

View File

@@ -17,7 +17,7 @@ from ...models import WEIMembership, Bus
WORDS = { WORDS = {
'list': [ 'list': [
'Fiesta', 'Graillance', 'Move it move it', 'Calme', 'Nert et geek', 'Jeux de rôles et danse rock', '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', 'Strass et paillettes', 'Spectaculaire', 'Splendide', 'Flow inégalable', 'Rap', 'Battles légendaires',
'Techno', 'Alcool', 'Kiffeur·euse', 'Rugby', 'Médiéval', 'Festif', 'Techno', 'Alcool', 'Kiffeur·euse', 'Rugby', 'Médiéval', 'Festif',
'Stylé', 'Chipie', 'Rétro', 'Vache', 'Farfadet', 'Fanfare', 'Stylé', 'Chipie', 'Rétro', 'Vache', 'Farfadet', 'Fanfare',
@@ -57,7 +57,7 @@ WORDS = {
42: "Un burgouzz de valouzz", 42: "Un burgouzz de valouzz",
47: "Un ocarina (pour me téléporter hors de ce bourbier)", 47: "Un ocarina (pour me téléporter hors de ce bourbier)",
48: "Des paillettes, un micro de karaoké et une enceinte bluetooth", 48: "Des paillettes, un micro de karaoké et une enceinte bluetooth",
45: "", 45: "Un kebab",
44: "Une 86 et un caisson pour taper du pied", 44: "Une 86 et un caisson pour taper du pied",
46: "Une épée, un ballon et une tireuse", 46: "Une épée, un ballon et une tireuse",
43: "Des lunettes de soleil", 43: "Des lunettes de soleil",
@@ -176,7 +176,33 @@ WORDS = {
49: "Soirée raclette !" 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 = { IMAGES = {
@@ -235,7 +261,7 @@ class WEISurveyForm2025(forms.Form):
all_preferred_words = WORDS['list'] all_preferred_words = WORDS['list']
rng.shuffle(all_preferred_words) rng.shuffle(all_preferred_words)
self.fields["words"].choices = [(w, w) for w in 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()) questions = list(WORDS['questions'].items())
idx = information.step - 1 idx = information.step - 1
if idx < len(questions): if idx < len(questions):
@@ -251,6 +277,15 @@ class WEISurveyForm2025(forms.Form):
widget=OptionalImageRadioSelect(images=IMAGES.get(q, {})), widget=OptionalImageRadioSelect(images=IMAGES.get(q, {})),
required=True, 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): def clean_words(self):
data = self.cleaned_data['words'] data = self.cleaned_data['words']
@@ -377,7 +412,7 @@ class WEISurvey2025(WEISurvey):
setattr(self.information, "word" + str(i), word) setattr(self.information, "word" + str(i), word)
self.information.step += 1 self.information.step += 1
self.save() self.save()
else: elif 1 <= self.information.step <= len(WORDS['questions']):
questions = list(WORDS['questions'].keys()) questions = list(WORDS['questions'].keys())
idx = self.information.step - 1 idx = self.information.step - 1
if idx < len(questions): if idx < len(questions):
@@ -385,6 +420,13 @@ class WEISurvey2025(WEISurvey):
setattr(self.information, q, form.cleaned_data[q]) setattr(self.information, q, form.cleaned_data[q])
self.information.step += 1 self.information.step += 1
self.save() 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 @classmethod
def get_algorithm_class(cls): def get_algorithm_class(cls):
@@ -394,7 +436,7 @@ class WEISurvey2025(WEISurvey):
""" """
The survey is complete once the bus is chosen. 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 @classmethod
@lru_cache() @lru_cache()

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

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">
<form method="post"> <form id="registration-form" method="post">
{% csrf_token %} {% csrf_token %}
{{ form|crispy }} {{ form|crispy }}
{{ membership_form|crispy }} {{ membership_form|crispy }}
@@ -22,6 +22,46 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endblock %} {% endblock %}
{% block extrajavascript %} {% 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 || input.type === "hidden" || input.disabled || input.readOnly) {
return;
}
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 %} {% if not object.membership %}
<script> <script>
$(document).ready(function () { $(document).ready(function () {

View File

@@ -53,9 +53,11 @@ class TestWEIAlgorithm(TestCase):
birth_date='2000-01-01', birth_date='2000-01-01',
) )
information = WEISurveyInformation2025(registration) 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'])) 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) information.save(registration)
registration.save() registration.save()
@@ -87,7 +89,7 @@ class TestWEIAlgorithm(TestCase):
setattr(information, f'word{j}', random.choice(WORDS['list'])) setattr(information, f'word{j}', random.choice(WORDS['list']))
for q in WORDS['questions']: for q in WORDS['questions']:
setattr(information, q, random.choice(list(WORDS['questions'][q][1].keys()))) 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) information.save(registration)
registration.save() registration.save()
survey = WEISurvey2025(registration) survey = WEISurvey2025(registration)

View File

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

View File

@@ -30,6 +30,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
<link rel="stylesheet" href="{% static "font-awesome/css/font-awesome.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="{% 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 #} {# JQuery, Bootstrap and Turbolinks JavaScript #}
<script src="{% static "jquery/jquery.min.js" %}"></script> <script src="{% static "jquery/jquery.min.js" %}"></script>
<script src="{% static "popper.js/umd/popper.min.js" %}"></script> <script src="{% static "popper.js/umd/popper.min.js" %}"></script>
@@ -41,6 +43,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
{# Translation in javascript files #} {# Translation in javascript files #}
<script src="{% static "js/jsi18n/"|add:LANGUAGE_CODE|add:".js" %}"></script> <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 extra ressources are needed for a form, load here #}
{% if form.media %} {% if form.media %}
{{ form.media }} {{ form.media }}

View File

@@ -19,7 +19,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endblocktrans %} {% endblocktrans %}
</div> </div>
<form method="post"> <form method="post" id="profile_form">
{% csrf_token %} {% csrf_token %}
{{ form|crispy }} {{ form|crispy }}
{{ profile_form|crispy }} {{ profile_form|crispy }}
@@ -31,3 +31,45 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
</div> </div>
{% endblock %} {% 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 || input.type === "hidden" || input.disabled || input.readOnly) {
return;
}
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 %}