1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-06-27 20:22:15 +02:00

Compare commits

..

1 Commits

Author SHA1 Message Date
89cc03141b allow search with club name 2025-06-12 18:48:29 +02:00
36 changed files with 1989 additions and 5774 deletions

View File

@ -1,19 +0,0 @@
# Generated by Django 4.2.20 on 2025-05-08 19:07
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('activity', '0006_guest_school'),
]
operations = [
migrations.AlterField(
model_name='guest',
name='activity',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='activity.activity'),
),
]

View File

@ -234,7 +234,7 @@ class Guest(models.Model):
"""
activity = models.ForeignKey(
Activity,
on_delete=models.CASCADE,
on_delete=models.PROTECT,
related_name='+',
)

View File

@ -95,23 +95,5 @@ SPDX-License-Identifier: GPL-3.0-or-later
errMsg(xhr.responseJSON);
});
});
$("#delete_activity").click(function () {
if (!confirm("{% trans 'Are you sure you want to delete this activity?' %}")) {
return;
}
$.ajax({
url: "/api/activity/activity/{{ activity.pk }}/",
type: "DELETE",
headers: {
"X-CSRFTOKEN": CSRF_TOKEN
}
}).done(function () {
addMsg("{% trans 'Activity deleted' %}", "success");
window.location.href = "/activity/"; // Redirige vers la liste des activités
}).fail(function (xhr) {
errMsg(xhr.responseJSON);
});
});
</script>
{% endblock %}

View File

@ -70,10 +70,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if ".change_"|has_perm:activity %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_update' pk=activity.pk %}" data-turbolinks="false"> {% trans "edit"|capfirst %}</a>
{% endif %}
{% if not activity.valid and ".delete_"|has_perm:activity %}
<a class="btn btn-danger btn-sm my-1" id="delete_activity"> {% trans "delete"|capfirst %} </a>
{% endif %}
{% if activity.activity_type.can_invite and not activity_started and activity.valid %}
{% if activity.activity_type.can_invite and not activity_started %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_invite' pk=activity.pk %}" data-turbolinks="false"> {% trans "Invite" %}</a>
{% endif %}
{% endif %}

View File

@ -15,5 +15,4 @@ urlpatterns = [
path('<int:pk>/update/', views.ActivityUpdateView.as_view(), name='activity_update'),
path('new/', views.ActivityCreateView.as_view(), name='activity_create'),
path('calendar.ics', views.CalendarView.as_view(), name='calendar_ics'),
path('<int:pk>/delete', views.ActivityDeleteView.as_view(), name='delete_activity'),
]

View File

@ -9,7 +9,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import F, Q
from django.http import HttpResponse, JsonResponse
from django.http import HttpResponse
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.decorators import method_decorator
@ -153,34 +153,6 @@ class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
class ActivityDeleteView(View):
"""
Deletes an Activity
"""
def delete(self, request, pk):
try:
activity = Activity.objects.get(pk=pk)
activity.delete()
return JsonResponse({"message": "Activity deleted"})
except Activity.DoesNotExist:
return JsonResponse({"error": "Activity not found"}, status=404)
def dispatch(self, *args, **kwargs):
"""
Don't display the delete button if the user has no right to delete.
"""
if not self.request.user.is_authenticated:
return self.handle_no_permission()
activity = Activity.objects.get(pk=self.kwargs["pk"])
if not PermissionBackend.check_perm(self.request, "activity.delete_activity", activity):
raise PermissionDenied(_("You are not allowed to delete this activity."))
if activity.valid:
raise PermissionDenied(_("This activity is valid."))
return super().dispatch(*args, **kwargs)
class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
"""
Invite a Guest, The rules to invites someone are defined in `forms:activity.GuestForm`

View File

@ -63,7 +63,8 @@ class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, Li
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}))
qs = qs.filter(Q(**{f'name{suffix}': prefix + pattern})
| Q(**{f'owner__name{suffix}': prefix + pattern}))
else:
qs = qs.none()
search_table = qs.filter(PermissionBackend.filter_queryset(self.request, Food, 'view'))
@ -168,8 +169,7 @@ class BasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
template_name = "food/food_update.html"
def get_sample_object(self):
# We choose a club which may work or BDE else
food = BasicFood(
return BasicFood(
name="",
owner_id=1,
expiry_date=timezone.now(),
@ -178,14 +178,6 @@ class BasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
date_type='DLC',
)
for membership in self.request.user.memberships.all():
club_id = membership.club.id
food.owner_id = club_id
if PermissionBackend.check_perm(self.request, "food.add_basicfood", food):
return food
return food
@transaction.atomic
def form_valid(self, form):
if QRCode.objects.filter(qr_code_number=self.kwargs['slug']).count() > 0:
@ -236,22 +228,13 @@ class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
template_name = "food/food_update.html"
def get_sample_object(self):
# We choose a club which may work or BDE else
food = TransformedFood(
return TransformedFood(
name="",
owner_id=1,
expiry_date=timezone.now(),
is_ready=True,
)
for membership in self.request.user.memberships.all():
club_id = membership.club.id
food.owner_id = club_id
if PermissionBackend.check_perm(self.request, "food.add_transformedfood", food):
return food
return food
@transaction.atomic
def form_valid(self, form):
form.instance.expiry_date = timezone.now() + timedelta(days=3)
@ -263,10 +246,10 @@ class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
return reverse_lazy('food:transformedfood_view', kwargs={"pk": self.object.pk})
MAX_FORMS = 100
MAX_FORMS = 10
class ManageIngredientsView(LoginRequiredMixin, UpdateView):
class ManageIngredientsView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
A view to manage ingredient for a transformed food
"""
@ -297,14 +280,6 @@ class ManageIngredientsView(LoginRequiredMixin, UpdateView):
ingredient.end_of_life = _('Fully used in {meal}'.format(
meal=self.object.name))
ingredient.save()
# We recalculate new expiry date and allergens
self.object.expiry_date = self.object.creation_date + self.object.shelf_life
self.object.allergens.clear()
for ingredient in self.object.ingredients.iterator():
if not (ingredient.polymorphic_ctype.model == 'basicfood' and ingredient.date_type == 'DDM'):
self.object.expiry_date = min(self.object.expiry_date, ingredient.expiry_date)
self.object.allergens.set(self.object.allergens.union(ingredient.allergens.all()))
self.object.save(old_ingredients=old_ingredients, old_allergens=old_allergens)
return HttpResponseRedirect(self.get_success_url())

View File

@ -1,46 +0,0 @@
from django.db import migrations
def create_bda(apps, schema_editor):
"""
The club BDA is now pre-injected.
"""
Club = apps.get_model("member", "club")
NoteClub = apps.get_model("note", "noteclub")
Alias = apps.get_model("note", "alias")
ContentType = apps.get_model('contenttypes', 'ContentType')
polymorphic_ctype_id = ContentType.objects.get_for_model(NoteClub).id
Club.objects.get_or_create(
id=10,
name="BDA",
email="bda.ensparissaclay@gmail.com",
require_memberships=True,
membership_fee_paid=750,
membership_fee_unpaid=750,
membership_duration=396,
membership_start="2024-08-01",
membership_end="2025-09-30",
)
NoteClub.objects.get_or_create(
id=1937,
club_id=10,
polymorphic_ctype_id=polymorphic_ctype_id,
)
Alias.objects.get_or_create(
id=1937,
note_id=1937,
name="BDA",
normalized_name="bda",
)
class Migration(migrations.Migration):
dependencies = [
('member', '0013_auto_20240801_1436'),
]
operations = [
migrations.RunPython(create_bda),
]

View File

@ -1695,7 +1695,7 @@
"wei",
"weimembership"
],
"query": "{\"club\": [\"club\"]}",
"query": "[\"AND\", {\"club\": [\"club\"], \"club__weiclub__membership_end__gte\": [\"today\"]}, [\"OR\", {\"registration__soge_credit\": true}, {\"user__note__balance__gte\": {\"F\": [\"F\", \"fee\"]}}]]",
"type": "add",
"mask": 2,
"field": "",
@ -3998,358 +3998,6 @@
"description": "Créer une transaction de ou vers la note d'un club tant que la source reste au dessus de -50 €"
}
},
{
"model": "permission.permission",
"pk": 271,
"fields": {
"model": [
"wei",
"bus"
],
"query": "{\"wei\": [\"club\"]}",
"type": "change",
"mask": 3,
"field": "",
"permanent": false,
"description": "Modifier n'importe quel bus du wei"
}
},
{
"model": "permission.permission",
"pk": 272,
"fields": {
"model": [
"wei",
"bus"
],
"query": "{\"wei\": [\"club\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir tous les bus du wei"
}
},
{
"model": "permission.permission",
"pk": 273,
"fields": {
"model": [
"wei",
"busteam"
],
"query": "{\"bus__wei\": [\"club\"], \"bus__wei__membership_end__gte\": [\"today\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir toutes les équipes WEI"
}
},
{
"model": "permission.permission",
"pk": 274,
"fields": {
"model": [
"member",
"club"
],
"query": "{\"bus__wei\": [\"club\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir les informations de clubs des bus"
}
},
{
"model": "permission.permission",
"pk": 275,
"fields": {
"model": [
"member",
"club"
],
"query": "{\"bus__wei\": [\"club\"]}",
"type": "change",
"mask": 3,
"field": "",
"permanent": false,
"description": "Modifier les clubs des bus"
}
},
{
"model": "permission.permission",
"pk": 276,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club__bus__wei\": [\"club\"]}",
"type": "add",
"mask": 3,
"field": "",
"permanent": false,
"description": "Ajouter un⋅e membre à un club de bus"
}
},
{
"model": "permission.permission",
"pk": 277,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club__bus__wei\": [\"club\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir les adhérents d'un club de bus"
}
},
{
"model": "permission.permission",
"pk": 278,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club__bus__wei\": [\"club\"]}",
"type": "change",
"mask": 3,
"field": "",
"permanent": false,
"description": "Modifier l'adhésion d'un club de bus"
}
},
{
"model": "permission.permission",
"pk": 279,
"fields": {
"model": [
"note",
"note"
],
"query": "{\"noteclub__club__bus__wei\": [\"club\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir la note d'un club de bus"
}
},
{
"model": "permission.permission",
"pk": 280,
"fields": {
"model": [
"note",
"transaction"
],
"query": "[\"OR\", {\"source__noteclub__club__bus__wei\": [\"club\"]}, {\"destination__noteclub__club__bus__wei\": [\"club\"]}]",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir les transactions d'un club de bus"
}
},
{
"model": "permission.permission",
"pk": 281,
"fields": {
"model": [
"note",
"transaction"
],
"query": "[\"AND\", [\"OR\", {\"source__noteclub__club__bus__wei\": [\"club\"]}, {\"destination__noteclub__club__bus__wei\": [\"club\"]}], [\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 2000]}}, {\"valid\": false}]]",
"type": "add",
"mask": 3,
"field": "",
"permanent": false,
"description": "Créer une transaction d'un club de bus tant que la source reste au dessus de -20 €"
}
},
{
"model": "permission.permission",
"pk": 282,
"fields": {
"model": [
"note",
"transaction"
],
"query": "[\"AND\", [\"OR\", {\"source__noteclub__club\": [\"club\"]}, {\"destination__noteclub__club\": [\"club\"]}], [\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 2000]}}, {\"valid\": false}]]",
"type": "add",
"mask": 3,
"field": "",
"permanent": false,
"description": "Créer une transaction d'un WEI tant que la source reste au dessus de -20 €"
}
},
{
"model": "permission.permission",
"pk": 283,
"fields": {
"model": [
"auth",
"user"
],
"query": "{\"memberships__club__name\": \"Kfet\", \"memberships__roles__name\": \"Adh\u00e9rent\u22c5e Kfet\", \"memberships__date_start__lte\": [\"today\"], \"memberships__date_end__gte\": [\"today\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir n'importe quel⋅le utilisateur⋅rice qui est adhérent⋅e Kfet"
}
},
{
"model": "permission.permission",
"pk": 284,
"fields": {
"model": [
"member",
"club"
],
"query": "{\"bus\": [\"membership\", \"weimembership\", \"bus\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir les informations de club de son bus"
}
},
{
"model": "permission.permission",
"pk": 285,
"fields": {
"model": [
"member",
"club"
],
"query": "{\"bus\": [\"membership\", \"weimembership\", \"bus\"]}",
"type": "change",
"mask": 3,
"field": "",
"permanent": false,
"description": "Modifier le club de son bus"
}
},
{
"model": "permission.permission",
"pk": 286,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club__bus\": [\"membership\", \"weimembership\", \"bus\"]}",
"type": "add",
"mask": 3,
"field": "",
"permanent": false,
"description": "Ajouter un⋅e membre au club de son bus"
}
},
{
"model": "permission.permission",
"pk": 287,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club__bus\": [\"membership\", \"weimembership\", \"bus\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir les adhérents du club de son bus"
}
},
{
"model": "permission.permission",
"pk": 288,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club__bus\": [\"membership\", \"weimembership\", \"bus\"]}",
"type": "change",
"mask": 3,
"field": "",
"permanent": false,
"description": "Modifier l'adhésion au club de son bus"
}
},
{
"model": "permission.permission",
"pk": 289,
"fields": {
"model": [
"note",
"note"
],
"query": "{\"noteclub__club__bus\": [\"membership\", \"weimembership\", \"bus\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir la note du club de son bus"
}
},
{
"model": "permission.permission",
"pk": 290,
"fields": {
"model": [
"note",
"transaction"
],
"query": "[\"OR\", {\"source__noteclub__club__bus\": [\"membership\", \"weimembership\", \"bus\"]}, {\"destination__noteclub__club__bus\": [\"membership\", \"weimembership\", \"bus\"]}]",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir les transactions du club de son bus"
}
},
{
"model": "permission.permission",
"pk": 291,
"fields": {
"model": [
"wei",
"bus"
],
"query": "{\"pk\": [\"membership\", \"weimembership\", \"bus\", \"pk\"], \"wei__date_end__gte\": [\"today\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir mon bus"
}
},
{
"model": "permission.permission",
"pk": 292,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club__pk__lte\": 2}",
"type": "add",
"mask": 3,
"field": "",
"permanent": false,
"description": "Ajouter un membre au BDE ou à la Kfet"
}
},
{
"model": "permission.role",
"pk": 1,
@ -4504,8 +4152,8 @@
"name": "Pr\u00e9sident\u22c5e de club",
"permissions": [
62,
135,
142
142,
135
]
}
},
@ -4710,8 +4358,6 @@
"name": "GC WEI",
"permissions": [
22,
49,
62,
70,
72,
76,
@ -4736,23 +4382,7 @@
112,
113,
128,
130,
142,
269,
271,
272,
273,
274,
275,
276,
277,
278,
279,
280,
281,
282,
283,
292
130
]
}
},
@ -4771,14 +4401,7 @@
119,
120,
121,
122,
284,
285,
286,
287,
289,
290,
291
122
]
}
},
@ -4939,140 +4562,6 @@
]
}
},
{
"model": "permission.role",
"pk": 23,
"fields": {
"for_club": 2,
"name": "Darbonne",
"permissions": [
30,
31,
32
]
}
},
{
"model": "permission.role",
"pk": 24,
"fields": {
"for_club": null,
"name": "Staffeur⋅euse (S&L,Respo Tech,...)",
"permissions": []
}
},
{
"model": "permission.role",
"pk": 25,
"fields": {
"for_club": null,
"name": "Référent⋅e Bus",
"permissions": [
22,
84,
115,
117,
118,
119,
120,
121,
122,
284,
285,
286,
287,
289,
290,
291
]
}
},
{
"model": "permission.role",
"pk": 28,
"fields": {
"for_club": 10,
"name": "Trésorièr⸱e BDA",
"permissions": [
55,
56,
57,
58,
135,
143,
176,
177,
178,
243,
260,
261,
262,
263,
264,
265,
266,
267,
268,
269
]
}
},
{
"model": "permission.role",
"pk": 30,
"fields": {
"for_club": 10,
"name": "Respo sorties",
"permissions": [
49,
62,
141,
241,
242,
243
]
}
},
{
"model": "permission.role",
"pk": 31,
"fields": {
"for_club": 1,
"name": "Respo comm",
"permissions": [
135,
244
]
}
},
{
"model": "permission.role",
"pk": 32,
"fields": {
"for_club": 10,
"name": "Respo comm Art",
"permissions": [
135,
245
]
}
},
{
"model": "permission.role",
"pk": 33,
"fields": {
"for_club": 10,
"name": "Respo Jam",
"permissions": [
247,
250,
251,
252,
253,
254
]
}
},
{
"model": "wei.weirole",
"pk": 12,
@ -5107,15 +4596,5 @@
"model": "wei.weirole",
"pk": 18,
"fields": {}
},
{
"model": "wei.weirole",
"pk": 24,
"fields": {}
},
{
"model": "wei.weirole",
"pk": 25,
"fields": {}
}
]

View File

@ -10,7 +10,7 @@ from django.utils import timezone
from django.utils.crypto import get_random_string
from activity.models import Activity
from member.models import Club, Membership
from note.models import NoteUser, NoteClub
from note.models import NoteUser
from wei.models import WEIClub, Bus, WEIRegistration
@ -122,13 +122,10 @@ class TestPermissionDenied(TestCase):
def test_validate_weiregistration(self):
wei = WEIClub.objects.create(
name="WEI Test",
membership_start=date.today(),
date_start=date.today() + timedelta(days=1),
date_end=date.today() + timedelta(days=1),
parent_club=Club.objects.get(name="Kfet"),
)
NoteClub.objects.create(club=wei)
registration = WEIRegistration.objects.create(wei=wei, user=self.user, birth_date="2000-01-01")
response = self.client.get(reverse("wei:validate_registration", kwargs=dict(pk=registration.pk)))
self.assertEqual(response.status_code, 403)

View File

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

View File

@ -24,7 +24,6 @@ class WEIForm(forms.ModelForm):
"membership_end": DatePickerInput(),
"date_start": DatePickerInput(),
"date_end": DatePickerInput(),
"caution_amount": AmountInput(),
}
@ -40,11 +39,7 @@ class WEIRegistrationForm(forms.ModelForm):
class Meta:
model = WEIRegistration
fields = [
'user', 'soge_credit', 'birth_date', 'gender', 'clothing_size',
'health_issues', 'emergency_contact_name', 'emergency_contact_phone',
'first_year', 'information_json', 'caution_check'
]
exclude = ('wei', 'clothing_cut')
widgets = {
"user": Autocomplete(
User,
@ -54,30 +49,11 @@ class WEIRegistrationForm(forms.ModelForm):
'placeholder': 'Nom ...',
},
),
"birth_date": DatePickerInput(options={
'minDate': '1900-01-01',
'maxDate': '2100-01-01'
}),
"caution_check": forms.BooleanField(
required=False,
),
"birth_date": DatePickerInput(options={'minDate': '1900-01-01',
'maxDate': '2100-01-01'}),
}
class WEIRegistration2AForm(WEIRegistrationForm):
class Meta(WEIRegistrationForm.Meta):
fields = WEIRegistrationForm.Meta.fields + ['caution_type']
widgets = WEIRegistrationForm.Meta.widgets.copy()
widgets.update({
"caution_type": forms.RadioSelect(),
})
class WEIRegistration1AForm(WEIRegistrationForm):
class Meta(WEIRegistrationForm.Meta):
fields = WEIRegistrationForm.Meta.fields
class WEIChooseBusForm(forms.Form):
bus = forms.ModelMultipleChoiceField(
queryset=Bus.objects,
@ -96,7 +72,7 @@ class WEIChooseBusForm(forms.Form):
)
roles = forms.ModelMultipleChoiceField(
queryset=WEIRole.objects.filter(~Q(name="1A") & ~Q(name="GC WEI")),
queryset=WEIRole.objects.filter(~Q(name="1A")),
label=_("WEI Roles"),
help_text=_("Select the roles that you are interested in."),
initial=WEIRole.objects.filter(name="Adhérent⋅e WEI").all(),
@ -105,8 +81,13 @@ class WEIChooseBusForm(forms.Form):
class WEIMembershipForm(forms.ModelForm):
caution_check = forms.BooleanField(
required=False,
label=_("Caution check given"),
)
roles = forms.ModelMultipleChoiceField(
queryset=WEIRole.objects.filter(~Q(name="GC WEI")),
queryset=WEIRole.objects,
label=_("WEI Roles"),
widget=CheckboxSelectMultiple(),
)

View File

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

View File

@ -121,13 +121,6 @@ class WEISurveyAlgorithm:
"""
raise NotImplementedError
@classmethod
def get_bus_information_form(cls):
"""
The class of the form to update the bus information.
"""
raise NotImplementedError
class WEISurvey:
"""

View File

@ -1,347 +0,0 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import time
import json
from functools import lru_cache
from random import Random
from django import forms
from django.db import transaction
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInformation
from ...models import WEIMembership, Bus
WORDS = [
'13 organisé', '3ième mi temps', 'Années 2000', 'Apéro', 'BBQ', 'BP', 'Beauf', 'Binge drinking', 'Bon enfant',
'Cartouche', 'Catacombes', 'Chansons paillardes', 'Chansons populaires', 'Chanteur', 'Chartreuse', 'Chill',
'Core', 'DJ', 'Dancefloor', 'Danse', 'David Guetta', 'Disco', 'Eau de vie', 'Électro', 'Escalade', 'Familial',
'Fanfare', 'Fracassage', 'Féria', 'Hard rock', 'Hoeggarden', 'House', 'Huit-six', 'IPA', 'Inclusif', 'Inferno',
'Introverti', 'Jager bomb', 'Jazz', 'Jeux d\'alcool', 'Jeux de rôles', 'Jeux vidéo', 'Jul', 'Jus de fruit',
'Karaoké', 'LGBTQI+', 'Lady Gaga', 'Loup garou', 'Morning beer', 'Métal', 'Nuit blanche', 'Ovalie', 'Psychedelic',
'Pétanque', 'Rave', 'Reggae', 'Rhum', 'Ricard', 'Rock', 'Rosé', 'Rétro', 'Séducteur', 'Techno', 'Thérapie taxi',
'Théâtre', 'Trap', 'Turn up', 'Underground', 'Volley', 'Wati B', 'Zinédine Zidane',
]
class WEISurveyForm2025(forms.Form):
"""
Survey form for the year 2025.
Members choose 20 words, from which we calculate the best associated bus.
"""
word = forms.ChoiceField(
label=_("Choose a word:"),
widget=forms.RadioSelect(),
)
def set_registration(self, registration):
"""
Filter the bus selector with the buses of the current WEI.
"""
information = WEISurveyInformation2025(registration)
if not information.seed:
information.seed = int(1000 * time.time())
information.save(registration)
registration._force_save = True
registration.save()
if self.data:
self.fields["word"].choices = [(w, w) for w in WORDS]
if self.is_valid():
return
rng = Random((information.step + 1) * information.seed)
buses = WEISurveyAlgorithm2025.get_buses()
informations = {bus: WEIBusInformation2025(bus) for bus in buses}
scores = sum((list(informations[bus].scores.values()) for bus in buses), [])
if scores:
average_score = sum(scores) / len(scores)
else:
average_score = 0
preferred_words = {bus: [word for word in WORDS
if informations[bus].scores[word] >= average_score]
for bus in buses}
# Correction : proposer plusieurs mots différents à chaque étape
n_choices = 4 # Nombre de mots à proposer à chaque étape
all_preferred_words = set()
for bus_words in preferred_words.values():
all_preferred_words.update(bus_words)
all_preferred_words = list(all_preferred_words)
rng.shuffle(all_preferred_words)
words = all_preferred_words[:n_choices]
self.fields["word"].choices = [(w, w) for w in words]
class WEIBusInformation2025(WEIBusInformation):
"""
For each word, the bus has a score
"""
scores: dict
def __init__(self, bus):
self.scores = {}
for word in WORDS:
self.scores[word] = 0
super().__init__(bus)
class BusInformationForm2025(forms.ModelForm):
class Meta:
model = Bus
fields = ['information_json']
widgets = {}
def __init__(self, *args, words=None, **kwargs):
super().__init__(*args, **kwargs)
initial_scores = {}
if self.instance and self.instance.information_json:
try:
info = json.loads(self.instance.information_json)
initial_scores = info.get("scores", {})
except (json.JSONDecodeError, TypeError, AttributeError):
initial_scores = {}
if words is None:
words = WORDS
self.words = words
choices = [(i, str(i)) for i in range(6)] # [(0, '0'), (1, '1'), ..., (5, '5')]
for word in words:
self.fields[word] = forms.TypedChoiceField(
label=word,
choices=choices,
coerce=int,
initial=initial_scores.get(word, 0),
required=True,
widget=forms.RadioSelect,
help_text=_("Rate between 0 and 5."),
)
def clean(self):
cleaned_data = super().clean()
scores = {}
for word in self.words:
value = cleaned_data.get(word)
if value is not None:
scores[word] = value
# On encode en JSON
cleaned_data['information_json'] = json.dumps({"scores": scores})
return cleaned_data
class WEISurveyInformation2025(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.
"""
# Random seed that is stored at the first time to ensure that words are generated only once
seed = 0
step = 0
def __init__(self, registration):
for i in range(1, 21):
setattr(self, "word" + str(i), None)
super().__init__(registration)
class WEISurvey2025(WEISurvey):
"""
Survey for the year 2025.
"""
@classmethod
def get_year(cls):
return 2025
@classmethod
def get_survey_information_class(cls):
return WEISurveyInformation2025
def get_form_class(self):
return WEISurveyForm2025
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):
word = form.cleaned_data["word"]
self.information.step += 1
setattr(self.information, "word" + str(self.information.step), word)
self.save()
@classmethod
def get_algorithm_class(cls):
return WEISurveyAlgorithm2025
def is_complete(self) -> bool:
"""
The survey is complete once the bus is chosen.
"""
return self.information.step == 20
@classmethod
@lru_cache()
def word_mean(cls, word):
"""
Calculate the mid-score given by all buses.
"""
buses = cls.get_algorithm_class().get_buses()
return sum([cls.get_algorithm_class().get_bus_information(bus).scores[word] for bus in buses]) / buses.count()
@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 = sum(bus_info.scores[getattr(self.information, 'word' + str(i))]
- self.word_mean(getattr(self.information, 'word' + str(i))) for i in range(1, 21)) / 20
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):
cls.word_mean.cache_clear()
return super().clear_cache()
class WEISurveyAlgorithm2025(WEISurveyAlgorithm):
"""
The algorithm class for the year 2025.
We use Gale-Shapley algorithm to attribute 1y students into buses.
"""
@classmethod
def get_survey_class(cls):
return WEISurvey2025
@classmethod
def get_bus_information_class(cls):
return WEIBusInformation2025
@classmethod
def get_bus_information_form(cls):
return BusInformationForm2025
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
WEISurvey2025.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

@ -1,18 +0,0 @@
# Generated by Django 4.2.21 on 2025-05-25 12:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wei', '0010_remove_weiregistration_specific_diet'),
]
operations = [
migrations.AlterField(
model_name='weiclub',
name='year',
field=models.PositiveIntegerField(default=2025, unique=True, verbose_name='year'),
),
]

View File

@ -1,20 +0,0 @@
# Generated by Django 4.2.21 on 2025-05-29 16:16
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('member', '0014_create_bda'),
('wei', '0011_alter_weiclub_year'),
]
operations = [
migrations.AddField(
model_name='bus',
name='club',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bus', to='member.club', verbose_name='club'),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 4.2.21 on 2025-06-01 21:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wei', '0012_bus_club'),
]
operations = [
migrations.AddField(
model_name='weiclub',
name='caution_amount',
field=models.PositiveIntegerField(default=0, verbose_name='caution amount'),
),
migrations.AddField(
model_name='weiregistration',
name='caution_type',
field=models.CharField(choices=[('check', 'Check'), ('note', 'Note transaction')], default='check', max_length=16, verbose_name='caution type'),
),
]

View File

@ -33,11 +33,6 @@ class WEIClub(Club):
verbose_name=_("date end"),
)
caution_amount = models.PositiveIntegerField(
verbose_name=_("caution amount"),
default=0,
)
class Meta:
verbose_name = _("WEI")
verbose_name_plural = _("WEI")
@ -77,15 +72,6 @@ class Bus(models.Model):
default=50,
)
club = models.OneToOneField(
Club,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="bus",
verbose_name=_("club"),
)
description = models.TextField(
blank=True,
default="",
@ -202,16 +188,6 @@ class WEIRegistration(models.Model):
verbose_name=_("Caution check given")
)
caution_type = models.CharField(
max_length=16,
choices=(
('check', _("Check")),
('note', _("Note transaction")),
),
default='check',
verbose_name=_("caution type"),
)
birth_date = models.DateField(
verbose_name=_("birth date"),
)

View File

@ -98,7 +98,7 @@ class WEIRegistrationTable(tables.Table):
if not hasperm:
return format_html("<span class='no-perm'></span>")
url = reverse_lazy('wei:wei_update_registration', args=(record.pk,)) + '?validate=true'
url = reverse_lazy('wei:validate_registration', args=(record.pk,))
text = _('Validate')
if record.fee > record.user.note.balance and not record.soge_credit:
btn_class = 'btn-secondary'

View File

@ -40,20 +40,22 @@ SPDX-License-Identifier: GPL-3.0-or-later
<dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_fee_paid|pretty_money }}</dd>
{% else %}
{% with bde_kfet_fee=club.parent_club.membership_fee_paid|add:club.parent_club.parent_club.membership_fee_paid %}
<dt class="col-xl-6">{% trans 'WEI fee (paid students)'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_fee_paid|pretty_money }}
<dd class="col-xl-6">{{ club.membership_fee_paid|add:bde_kfet_fee|pretty_money }}
<i class="fa fa-question-circle"
title="{% trans "The BDE membership is included in the WEI registration." %}"></i></dd>
{% endwith %}
{% with bde_kfet_fee=club.parent_club.membership_fee_unpaid|add:club.parent_club.parent_club.membership_fee_unpaid %}
<dt class="col-xl-6">{% trans 'WEI fee (unpaid students)'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_fee_unpaid|pretty_money }}
<dd class="col-xl-6">{{ club.membership_fee_unpaid|add:bde_kfet_fee|pretty_money }}
<i class="fa fa-question-circle"
title="{% trans "The BDE membership is included in the WEI registration." %}"></i></dd>
{% endwith %}
{% endif %}
{% endif %}
{% if club.caution_amount > 0 %}
<dt class="col-xl-6">{% trans 'Caution amount'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.caution_amount|pretty_money }}</dd>
{% endif %}
{% if "note.view_note"|has_perm:club.note %}
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.note.balance | pretty_money }}</dd>

View File

@ -16,14 +16,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div>
<div class="card-footer text-center">
{% if object.club %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_detail' pk=object.club.pk %}"
data-turbolinks="false">{% trans "View club" %}</a>
{% 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:add_team' pk=object.pk %}"
data-turbolinks="false">{% trans "Add team" %}</a>
</div>

View File

@ -18,8 +18,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card-footer text-center">
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus' pk=bus.pk %}"
data-turbolinks="false">{% trans "Edit" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:manage_bus' pk=bus.pk %}"
data-turbolinks="false">{% trans "View" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_team' pk=bus.pk %}"
data-turbolinks="false">{% trans "Add team" %}</a>
</div>

View File

@ -13,17 +13,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card-body">
<form method="post">
{% csrf_token %}
{{ form.media }}
{{ form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
</form>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function () {
if (window.jscolor && jscolor.install) {
jscolor.install();
}
});
</script>
{% endblock %}

View File

@ -98,8 +98,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if can_validate_1a %}
<a href="{% url 'wei:wei_1A_list' pk=object.pk %}" class="btn btn-block btn-info">{% trans "Attribute buses" %}</a>
{% endif %}
{% endblock %}
{% block extrajavascript %}

View File

@ -143,35 +143,25 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endblocktrans %}
</div>
{% else %}
<div class="alert {% if registration.user.note.balance < fee %}alert-danger{% else %}alert-success{% endif %}">
<h5>{% trans "Required payments:" %}</h5>
<ul>
<li>{% blocktrans trimmed with amount=fee|pretty_money %}
Membership fees: {{ amount }}
{% endblocktrans %}</li>
{% if registration.caution_type == 'note' %}
<li>{% blocktrans trimmed with amount=club.caution_amount|pretty_money %}
Deposit (by Note transaction): {{ amount }}
{% endblocktrans %}</li>
<li><strong>{% blocktrans trimmed with total=total_needed|pretty_money %}
Total needed: {{ total }}
{% endblocktrans %}</strong></li>
{% if registration.user.note.balance < fee %}
<div class="alert alert-danger">
{% with pretty_fee=fee|pretty_money %}
{% blocktrans trimmed with balance=registration.user.note.balance|pretty_money %}
The note don't have enough money ({{ balance }}, {{ pretty_fee }} required).
The registration may fail if you don't credit the note now.
{% endblocktrans %}
{% endwith %}
</div>
{% else %}
<li>{% blocktrans trimmed with amount=club.caution_amount|pretty_money %}
Deposit (by check): {{ amount }}
{% endblocktrans %}</li>
<li><strong>{% blocktrans trimmed with total=fee|pretty_money %}
Total needed: {{ total }}
{% endblocktrans %}</strong></li>
{% endif %}
</ul>
<p>{% blocktrans trimmed with balance=registration.user.note.balance|pretty_money %}
Current balance: {{ balance }}
{% endblocktrans %}</p>
<div class="alert alert-success">
{% blocktrans trimmed with pretty_fee=fee|pretty_money %}
The note has enough money ({{ pretty_fee }} required), the registration is possible.
{% endblocktrans %}
</div>
{% endif %}
{% endif %}
{% if not registration.caution_check and not registration.first_year and registration.caution_type == 'check' %}
{% if not registration.caution_check and not registration.first_year %}
<div class="alert alert-danger">
{% trans "The user didn't give her/his caution check." %}
</div>

View File

@ -6,6 +6,8 @@ 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
@ -127,3 +129,44 @@ class TestWEIAlgorithm(TestCase):
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

@ -1,111 +0,0 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import random
from django.contrib.auth.models import User
from django.test import TestCase
from ..forms.surveys.wei2025 import WEIBusInformation2025, WEISurvey2025, WORDS, WEISurveyInformation2025
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.wei = WEIClub.objects.create(
name="WEI 2025",
email="wei2025@example.com",
date_start='2025-09-12',
date_end='2025-09-14',
year=2025,
membership_start='2025-06-01'
)
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 = WEIBusInformation2025(bus)
for word in WORDS:
information.scores[word] = random.randint(0, 101)
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 = WEISurveyInformation2025(registration)
for j in range(1, 21):
setattr(information, f'word{j}', random.choice(WORDS))
information.step = 20
information.save(registration)
registration.save()
# Run algorithm
WEISurvey2025.get_algorithm_class()().run_algorithm()
# Ensure that everyone has its first choice
for r in WEIRegistration.objects.filter(wei=self.wei).all():
survey = WEISurvey2025(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 = WEISurveyInformation2025(registration)
for j in range(1, 21):
setattr(information, f'word{j}', random.choice(WORDS))
information.step = 20
information.save(registration)
registration.save()
# Run algorithm
WEISurvey2025.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 = WEISurvey2025(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 %

View File

@ -126,7 +126,6 @@ class TestWEIRegistration(TestCase):
year=self.year + 1,
date_start=str(self.year + 1) + "-09-01",
date_end=str(self.year + 1) + "-09-03",
caution_amount=12000,
))
qs = WEIClub.objects.filter(name="Create WEI Test", year=self.year + 1)
self.assertTrue(qs.exists())
@ -161,7 +160,6 @@ class TestWEIRegistration(TestCase):
membership_end="2000-09-30",
date_start="2000-09-01",
date_end="2000-09-03",
caution_amount=12000,
))
qs = WEIClub.objects.filter(name="Update WEI Test", id=self.wei.id)
self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=self.wei.pk)), 302, 200)
@ -320,7 +318,6 @@ class TestWEIRegistration(TestCase):
bus=[],
team=[],
roles=[],
caution_type='check'
))
self.assertEqual(response.status_code, 200)
self.assertFalse(response.context["membership_form"].is_valid())
@ -337,8 +334,7 @@ class TestWEIRegistration(TestCase):
emergency_contact_phone='+33123456789',
bus=[self.bus.id],
team=[self.team.id],
roles=[role.id for role in WEIRole.objects.filter(~Q(name="1A") & ~Q(name="GC WEI")).all()],
caution_type='check'
roles=[role.id for role in WEIRole.objects.filter(~Q(name="1A")).all()],
))
qs = WEIRegistration.objects.filter(user_id=user.id)
self.assertTrue(qs.exists())
@ -358,7 +354,6 @@ class TestWEIRegistration(TestCase):
bus=[self.bus.id],
team=[self.team.id],
roles=[role.id for role in WEIRole.objects.filter(~Q(name="1A")).all()],
caution_type='check'
))
self.assertEqual(response.status_code, 200)
self.assertTrue("This user is already registered to this WEI." in str(response.context["form"].errors))
@ -511,12 +506,11 @@ class TestWEIRegistration(TestCase):
team=[self.team.id],
roles=[role.id for role in WEIRole.objects.filter(name="Adhérent⋅e WEI").all()],
information_json=self.registration.information_json,
caution_type='check'
)
)
qs = WEIRegistration.objects.filter(user_id=self.user.id, soge_credit=False, clothing_size="M")
self.assertTrue(qs.exists())
self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=qs.get().wei.pk)), 302, 200)
self.assertRedirects(response, reverse("wei:validate_registration", kwargs=dict(pk=qs.get().pk)), 302, 200)
# Check the page when the registration is already validated
membership = WEIMembership(
@ -566,12 +560,11 @@ class TestWEIRegistration(TestCase):
team=[self.team.id],
roles=[role.id for role in WEIRole.objects.filter(name="Adhérent⋅e WEI").all()],
information_json=self.registration.information_json,
caution_type='check'
)
)
qs = WEIRegistration.objects.filter(user_id=self.user.id, clothing_size="L")
self.assertTrue(qs.exists())
self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=qs.get().wei.pk)), 302, 200)
self.assertRedirects(response, reverse("wei:validate_registration", kwargs=dict(pk=qs.get().pk)), 302, 200)
# Test invalid form
response = self.client.post(
@ -590,7 +583,6 @@ class TestWEIRegistration(TestCase):
team=[],
roles=[],
information_json=self.registration.information_json,
caution_type='check'
)
)
self.assertFalse(response.context["membership_form"].is_valid())
@ -632,7 +624,7 @@ class TestWEIRegistration(TestCase):
second_bus = Bus.objects.create(wei=self.wei, name="Second bus")
second_team = BusTeam.objects.create(bus=second_bus, name="Second team", color=42)
response = self.client.post(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk)), dict(
roles=[WEIRole.objects.get(name="Adhérent⋅e WEI").id],
roles=[WEIRole.objects.get(name="GC WEI").id],
bus=self.bus.pk,
team=second_team.pk,
credit_type=4, # Bank transfer
@ -640,14 +632,13 @@ class TestWEIRegistration(TestCase):
last_name="admin",
first_name="admin",
bank="Société générale",
caution_check=True,
))
self.assertEqual(response.status_code, 200)
self.assertFalse(response.context["form"].is_valid())
self.assertTrue("This team doesn&#x27;t belong to the given bus." in str(response.context["form"].errors))
response = self.client.post(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk)), dict(
roles=[WEIRole.objects.get(name="Adhérent⋅e WEI").id],
roles=[WEIRole.objects.get(name="GC WEI").id],
bus=self.bus.pk,
team=self.team.pk,
credit_type=4, # Bank transfer
@ -655,10 +646,8 @@ class TestWEIRegistration(TestCase):
last_name="admin",
first_name="admin",
bank="Société générale",
caution_check=True,
))
self.assertRedirects(response, reverse("wei:wei_registrations", kwargs=dict(pk=self.registration.wei.pk)), 302, 200)
# Check if the membership is successfully created
membership = WEIMembership.objects.filter(user_id=self.user.id, club=self.wei)
self.assertTrue(membership.exists())
@ -778,7 +767,7 @@ class TestDefaultWEISurvey(TestCase):
WEISurvey.update_form(None, None)
self.assertEqual(CurrentSurvey.get_algorithm_class().get_survey_class(), CurrentSurvey)
self.assertEqual(CurrentSurvey.get_year(), 2025)
self.assertEqual(CurrentSurvey.get_year(), 2024)
class TestWeiAPI(TestAPI):

View File

@ -4,7 +4,7 @@
from django.urls import path
from .views import CurrentWEIDetailView, WEI1AListView, WEIListView, WEICreateView, WEIDetailView, WEIUpdateView, \
WEIRegistrationsView, WEIMembershipsView, MemberListRenderView, BusInformationUpdateView, \
WEIRegistrationsView, WEIMembershipsView, MemberListRenderView, \
BusCreateView, BusManageView, BusUpdateView, BusTeamCreateView, BusTeamManageView, BusTeamUpdateView, \
WEIAttributeBus1AView, WEIAttributeBus1ANextView, WEIRegister1AView, WEIRegister2AView, WEIUpdateRegistrationView, \
WEIDeleteRegistrationView, WEIValidateRegistrationView, WEISurveyView, WEISurveyEndView, WEIClosedView
@ -42,5 +42,4 @@ urlpatterns = [
path('detail/<int:pk>/closed/', WEIClosedView.as_view(), name="wei_closed"),
path('bus-1A/<int:pk>/', WEIAttributeBus1AView.as_view(), name="wei_bus_1A"),
path('bus-1A/next/<int:pk>/', WEIAttributeBus1ANextView.as_view(), name="wei_bus_1A_next"),
path('update-bus-info/<int:pk>/', BusInformationUpdateView.as_view(), name="update_bus_info"),
]

View File

@ -4,18 +4,16 @@
import os
import shutil
import subprocess
from datetime import date
from datetime import date, timedelta
from tempfile import mkdtemp
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import Q, Count
from django.db.models.functions.text import Lower
from django import forms
from django.http import HttpResponse, Http404
from django.shortcuts import redirect
from django.template.loader import render_to_string
@ -35,7 +33,7 @@ from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from .forms.registration import WEIChooseBusForm
from .models import WEIClub, WEIRegistration, WEIMembership, Bus, BusTeam, WEIRole
from .forms import WEIForm, WEIRegistrationForm, WEIRegistration1AForm, WEIRegistration2AForm, BusForm, BusTeamForm, WEIMembership1AForm, \
from .forms import WEIForm, WEIRegistrationForm, BusForm, BusTeamForm, WEIMembership1AForm, \
WEIMembershipForm, CurrentSurvey
from .tables import BusRepartitionTable, BusTable, BusTeamTable, WEITable, WEIRegistrationTable, \
WEIRegistration1ATable, WEIMembershipTable
@ -443,10 +441,6 @@ class BusTeamCreateView(ProtectQuerysetMixin, ProtectedCreateView):
self.object.refresh_from_db()
return reverse_lazy("wei:manage_bus_team", kwargs={"pk": self.object.pk})
def get_template_names(self):
names = super().get_template_names()
return names
class BusTeamUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
@ -479,10 +473,6 @@ class BusTeamUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
self.object.refresh_from_db()
return reverse_lazy("wei:manage_bus_team", kwargs={"pk": self.object.pk})
def get_template_names(self):
names = super().get_template_names()
return names
class BusTeamManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
@ -510,7 +500,7 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView):
Register a new user to the WEI
"""
model = WEIRegistration
form_class = WEIRegistration1AForm
form_class = WEIRegistrationForm
extra_context = {"title": _("Register first year student to the WEI")}
def get_sample_object(self):
@ -556,17 +546,9 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView):
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.fields["user"].initial = self.request.user
# Cacher les champs pendant l'inscription initiale
if "first_year" in form.fields:
del form.fields["first_year"]
if "caution_check" in form.fields:
del form.fields["caution_check"]
if "information_json" in form.fields:
del form.fields["information_json"]
if "caution_type" in form.fields:
del form.fields["caution_type"]
return form
@transaction.atomic
@ -604,7 +586,7 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
Register an old user to the WEI
"""
model = WEIRegistration
form_class = WEIRegistration2AForm
form_class = WEIRegistrationForm
extra_context = {"title": _("Register old student to the WEI")}
def get_sample_object(self):
@ -662,20 +644,10 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
form.fields["soge_credit"].disabled = True
form.fields["soge_credit"].help_text = _("You already opened an account in the Société générale.")
# Cacher les champs pendant l'inscription initiale
if "first_year" in form.fields:
del form.fields["first_year"]
if "caution_check" in form.fields:
del form.fields["caution_check"]
if "information_json" in form.fields:
del form.fields["first_year"]
del form.fields["information_json"]
# S'assurer que le champ caution_type est obligatoire
if "caution_type" in form.fields:
form.fields["caution_type"].required = True
form.fields["caution_type"].help_text = _("Choose how you want to pay the deposit")
form.fields["caution_type"].widget = forms.RadioSelect(choices=form.fields["caution_type"].choices)
return form
@transaction.atomic
@ -701,9 +673,6 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]]
information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]]
form.instance.information = information
# Sauvegarder le type de caution
form.instance.caution_type = form.cleaned_data["caution_type"]
form.instance.save()
if 'treasury' in settings.INSTALLED_APPS:
@ -733,15 +702,11 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
# We can't update a registration once the WEI is started and before the membership start date
if today >= wei.date_start or today < wei.membership_start:
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
# Store the validate parameter in the view's state
self.should_validate = request.GET.get('validate', False)
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["club"] = self.object.wei
# Pass the validate parameter to the template
context["should_validate"] = self.should_validate
if self.object.is_validated:
membership_form = self.get_membership_form(instance=self.object.membership,
@ -775,16 +740,6 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
# The auto-json-format may cause issues with the default field remove
if not PermissionBackend.check_perm(self.request, 'wei.change_weiregistration_information_json', self.object):
del form.fields["information_json"]
# Masquer le champ caution_check pour tout le monde dans le formulaire de modification
if "caution_check" in form.fields:
del form.fields["caution_check"]
# S'assurer que le champ caution_type est obligatoire pour les 2A+
if not self.object.first_year and "caution_type" in form.fields:
form.fields["caution_type"].required = True
form.fields["caution_type"].help_text = _("Choose how you want to pay the deposit")
form.fields["caution_type"].widget = forms.RadioSelect(choices=form.fields["caution_type"].choices)
return form
def get_membership_form(self, data=None, instance=None):
@ -804,30 +759,10 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
def form_valid(self, form):
# If the membership is already validated, then we update the bus and the team (and the roles)
if form.instance.is_validated:
try:
membership = form.instance.membership
if membership is None:
raise ValueError(_("No membership found for this registration"))
membership_form = self.get_membership_form(self.request.POST, instance=membership)
membership_form = self.get_membership_form(self.request.POST, form.instance.membership)
if not membership_form.is_valid():
return self.form_invalid(form)
# Vérifier que l'utilisateur a la permission de modifier le membership
# On vérifie d'abord si l'utilisateur a la permission générale de modification
if not self.request.user.has_perm("wei.change_weimembership"):
raise PermissionDenied(_("You don't have the permission to update memberships"))
# On vérifie ensuite les permissions spécifiques pour chaque champ modifié
for field_name in membership_form.changed_data:
perm = f"wei.change_weimembership_{field_name}"
if not self.request.user.has_perm(perm):
raise PermissionDenied(_("You don't have the permission to update the field %(field)s") % {'field': field_name})
membership_form.save()
except (WEIMembership.DoesNotExist, ValueError, PermissionDenied) as e:
form.add_error(None, str(e))
return self.form_invalid(form)
# If it is not validated and if this is an old member, then we update the choices
elif not form.instance.first_year and PermissionBackend.check_perm(
self.request, "wei.change_weiregistration_information_json", self.object):
@ -842,10 +777,6 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]]
information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]]
form.instance.information = information
# Sauvegarder le type de caution pour les 2A+
if "caution_type" in form.cleaned_data:
form.instance.caution_type = form.cleaned_data["caution_type"]
form.instance.save()
return super().form_valid(form)
@ -856,8 +787,14 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
survey = CurrentSurvey(self.object)
if not survey.is_complete():
return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk})
# On redirige vers la validation uniquement si c'est explicitement demandé (et stocké dans la vue)
if self.should_validate and self.request.user.has_perm("wei.add_weimembership"):
if PermissionBackend.check_perm(self.request, "wei.add_weimembership", WEIMembership(
club=self.object.wei,
user=self.object.user,
date_start=date.today(),
date_end=date.today(),
fee=0,
registration=self.object,
)):
return reverse_lazy("wei:validate_registration", kwargs={"pk": self.object.pk})
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.wei.pk})
@ -899,23 +836,18 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
extra_context = {"title": _("Validate WEI registration")}
def get_sample_object(self):
"""
Return a sample object for permission checking
"""
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
return WEIMembership(
user=registration.user,
club=registration.wei,
date_start=registration.wei.date_start,
fee=registration.wei.membership_fee_paid if registration.user.profile.paid else registration.wei.membership_fee_unpaid,
# Add any fields needed for proper permission checking
user=registration.user,
date_start=date.today(),
date_end=date.today() + timedelta(days=1),
fee=0,
registration=registration,
)
def dispatch(self, request, *args, **kwargs):
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
wei = registration.wei
wei = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei
today = date.today()
# We can't validate anyone once the WEI is started and before the membership start date
if today >= wei.date_start or today < wei.membership_start:
@ -946,14 +878,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
date_start__gte=bde.membership_start,
).exists()
fee = registration.fee
context["fee"] = fee
# Calculer le montant total nécessaire (frais + caution si transaction)
total_needed = fee
if registration.caution_type == 'note':
total_needed += registration.wei.caution_amount
context["total_needed"] = total_needed
context["fee"] = registration.fee
form = context["form"]
if registration.soge_credit:
@ -965,7 +890,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
def get_form_class(self):
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
if registration.first_year and 'selected_bus_pk' not in registration.information:
if registration.first_year and 'sleected_bus_pk' not in registration.information:
return WEIMembership1AForm
return WEIMembershipForm
@ -975,24 +900,8 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
form.fields["last_name"].initial = registration.user.last_name
form.fields["first_name"].initial = registration.user.first_name
# Ajouter le champ caution_check uniquement pour les non-première année et le rendre obligatoire
if not registration.first_year:
if registration.caution_type == 'check':
form.fields["caution_check"] = forms.BooleanField(
required=True,
initial=registration.caution_check,
label=_("Caution check given"),
help_text=_("Please make sure the check is given before validating the registration")
)
else:
form.fields["caution_check"] = forms.BooleanField(
required=True,
initial=False,
label=_("Create deposit transaction"),
help_text=_("A transaction of %(amount).2f€ will be created from the user's Note account") % {
'amount': registration.wei.caution_amount / 100
}
)
if "caution_check" in form.fields:
form.fields["caution_check"].initial = registration.caution_check
if registration.soge_credit:
form.fields["credit_type"].disabled = True
@ -1076,20 +985,10 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
if credit_type is None or registration.soge_credit:
credit_amount = 0
# Calculer le montant total nécessaire (frais + caution si transaction)
total_needed = fee
if registration.caution_type == 'note':
total_needed += club.caution_amount
# Vérifier que l'utilisateur a assez d'argent pour tout payer
if not registration.soge_credit and user.note.balance + credit_amount < total_needed:
if not registration.soge_credit and user.note.balance + credit_amount < fee:
# Users must have money before registering to the WEI.
form.add_error('credit_type',
_("This user doesn't have enough money to join this club and pay the deposit. "
"Current balance: %(balance)d€, credit: %(credit)d€, needed: %(needed)d") % {
'balance': user.note.balance,
'credit': credit_amount,
'needed': total_needed}
)
_("This user don't have enough money to join this club, and can't have a negative balance."))
return super().form_invalid(form)
if credit_amount:
@ -1129,18 +1028,6 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
membership.refresh_from_db()
membership.roles.add(WEIRole.objects.get(name="Adhérent⋅e WEI"))
# Créer la transaction de caution si nécessaire
if registration.caution_type == 'note':
from note.models import Transaction
Transaction.objects.create(
source=user.note,
destination=club.note,
quantity=1,
amount=club.caution_amount,
reason=_("Caution %(name)s") % {'name': club.name},
valid=True,
)
return super().form_valid(form)
def get_success_url(self):
@ -1360,7 +1247,6 @@ class WEI1AListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableView):
def get_queryset(self, filter_permissions=True, **kwargs):
qs = super().get_queryset(filter_permissions, **kwargs)
qs = qs.filter(first_year=True, membership__isnull=False)
qs = qs.filter(wei=self.club)
qs = qs.order_by('-membership__bus')
return qs
@ -1403,48 +1289,8 @@ class WEIAttributeBus1ANextView(LoginRequiredMixin, RedirectView):
if not wei.exists():
raise Http404
wei = wei.get()
# On cherche d'abord les 1A qui ont une inscription validée (membership) mais pas de bus
qs = WEIRegistration.objects.filter(
wei=wei,
first_year=True,
membership__isnull=False,
membership__bus__isnull=True
)
# Parmi eux, on prend ceux qui ont répondu au questionnaire (ont un bus préféré)
qs = qs.filter(information_json__contains='selected_bus_pk')
if not qs.exists():
# Si on ne trouve personne, on affiche un message et on retourne à la liste
messages.info(self.request, _("No first year student without a bus found. Either all of them have a bus, or none has filled the survey yet."))
return reverse_lazy('wei:wei_1A_list', args=(wei.pk,))
# On redirige vers la page d'attribution pour le premier étudiant trouvé
qs = WEIRegistration.objects.filter(wei=wei, membership__isnull=False, membership__bus__isnull=True)
qs = qs.filter(information_json__contains='selected_bus_pk') # not perfect, but works...
if qs.exists():
return reverse_lazy('wei:wei_bus_1A', args=(qs.first().pk, ))
class BusInformationUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
model = Bus
def get_form_class(self):
return CurrentSurvey.get_algorithm_class().get_bus_information_form()
def dispatch(self, request, *args, **kwargs):
wei = self.get_object().wei
today = date.today()
# We can't update a bus once the WEI is started
if today >= wei.date_start:
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["club"] = self.object.wei
context["information"] = CurrentSurvey.get_algorithm_class().get_bus_information(self.object)
self.object.save()
return context
def get_success_url(self):
self.object.refresh_from_db()
return reverse_lazy("wei:manage_bus", kwargs={"pk": self.object.pk})
return reverse_lazy('wei:wei_1A_list', args=(wei.pk, ))

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -63,16 +63,8 @@ class ColorWidget(Widget):
def format_value(self, value):
if value is None:
value = 0xFFFFFF
if isinstance(value, str):
return value # Assume it's already a hex string like "#FFAA33"
try:
return "#{:06X}".format(value)
except Exception:
return "#FFFFFF"
def value_from_datadict(self, data, files, name):
val = super().value_from_datadict(data, files, name)
if val:
return int(val[1:], 16)
return None

View File

@ -1,5 +0,0 @@
<input type="text"
name="{{ widget.name }}"
value="{{ widget.value }}"
class="jscolor"
{% include "django/forms/widgets/attrs.html" %}>