1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-07-20 08:01:26 +02:00

Merge branch 'main' into family

This commit is contained in:
Ehouarn
2025-07-18 18:01:46 +02:00
40 changed files with 1576 additions and 2175 deletions

View File

@ -145,7 +145,7 @@ class AddIngredientForms(forms.ModelForm):
polymorphic_ctype__model="transformedfood",
is_ready=False,
end_of_life='',
).filter(PermissionBackend.filter_queryset(get_current_request(), TransformedFood, "change")).exclude(pk=pk)
).filter(PermissionBackend.filter_queryset(get_current_request(), Food, "change")).exclude(pk=pk)
class Meta:
model = TransformedFood

View File

@ -12,18 +12,21 @@ SPDX-License-Identifier: GPL-3.0-or-later
</h3>
<div class="card-body">
<ul>
{% if QR_code %}
<li> {{QR_code}} </li>
{% endif %}
{% for field, value in fields %}
<li> {{ field }} : {{ value }}</li>
{% endfor %}
{% if meals %}
<li> {% trans "Contained in" %} :
<li> {% trans "Contained in" %} :
{% for meal in meals %}
<a href="{% url "food:transformedfood_view" pk=meal.pk %}">{{ meal.name }}</a>{% if not forloop.last %},{% endif %}
<a href="{% url "food:transformedfood_view" pk=meal.pk %}">{{ meal.name }}</a>{% if not forloop.last %},{% endif %}
{% endfor %}
</li>
{% endif %}
{% if foods %}
<li> {% trans "Contain" %} :
<li> {% trans "Contain" %} :
{% for food in foods %}
<a href="{% url "food:food_view" pk=food.pk %}">{{ food.name }}</a>{% if not forloop.last %},{% endif %}
{% endfor %}
@ -31,23 +34,23 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endif %}
</ul>
{% if update %}
<a class="btn btn-sm btn-secondary" href="{% url "food:food_update" pk=food.pk %}">
{% trans "Update" %}
</a>
<a class="btn btn-sm btn-secondary" href="{% url "food:food_update" pk=food.pk %}">
{% trans "Update" %}
</a>
{% endif %}
{% if add_ingredient %}
<a class="btn btn-sm btn-primary" href="{% url "food:add_ingredient" pk=food.pk %}">
{% trans "Add to a meal" %}
</a>
<a class="btn btn-sm btn-primary" href="{% url "food:add_ingredient" pk=food.pk %}">
{% trans "Add to a meal" %}
</a>
{% endif %}
{% if manage_ingredients %}
<a class="btn btn-sm btn-secondary" href="{% url "food:manage_ingredients" pk=food.pk %}">
{% trans "Manage ingredients" %}
</a>
{% trans "Manage ingredients" %}
</a>
{% endif %}
<a class="btn btn-sm btn-primary" href="{% url "food:food_list" %}">
{% trans "Return to the food list" %}
</a>
<a class="btn btn-sm btn-primary" href="{% url "food:food_list" %}">
{% trans "Return to the food list" %}
</a>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@ -7,7 +7,52 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% load i18n %}
{% block content %}
{{ block.super }}
<div class="card bg-light">
<h3 class="card-header text-center">
{{ title }}
</h3>
<div class="card-body">
<style>
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
appearance: textfield;
padding: 6px;
border: 1px solid #ccc;
border-radius: 4px;
width: 100px;
}
</style>
<div class="d-flex align-items-center" style="max-width: 300px;">
<form method="get" action="{% url 'food:redirect_view' %}" class="d-flex w-100">
<input type="number" name="slug" placeholder="QR-code" required class="form-control form-control-sm" style="max-width: 120px;">
<button type="submit" class="btn btn-sm btn-primary">{% trans "View food" %}</button>
</form>
</div>
</div>
<div class="card-body">
<input id="searchbar" type="text" class="form-control"
placeholder="{% trans "Search by attribute such as name..." %}">
</div>
{% block extra_inside_card %}
{% endblock %}
<div id="dynamic-table">
{% if table.data %}
{% render_table table %}
{% else %}
<div class="card-body">
<div class="alert alert-warning">
{% trans "There is no results." %}
</div>
</div>
{% endif %}
</div>
</div>
<br>
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
@ -68,4 +113,20 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endfor %}
{% endif %}
</div>
{% endblock %}
<script>
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('goButton').addEventListener('click', function(event) {
event.preventDefault();
const slug = document.getElementById('slugInput').value;
if (slug && !isNaN(slug)) {
window.location.href = `/food/${slug}/`;
} else {
alert("Veuillez entrer un nombre valide.");
}
});
});
</script>
{% endblock %}

View File

@ -18,4 +18,5 @@ urlpatterns = [
path('detail/basic/<int:pk>', views.BasicFoodDetailView.as_view(), name='basicfood_view'),
path('detail/transformed/<int:pk>', views.TransformedFoodDetailView.as_view(), name='transformedfood_view'),
path('add/ingredient/<int:pk>', views.AddIngredientView.as_view(), name='add_ingredient'),
path('redirect/', views.QRCodeRedirectView.as_view(), name='redirect_view'),
]

View File

@ -10,6 +10,7 @@ from django.db.models import Q
from django.http import HttpResponseRedirect, Http404
from django.views.generic import DetailView, UpdateView, CreateView
from django.views.generic.list import ListView
from django.views.generic.base import RedirectView
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
@ -63,7 +64,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'))
@ -453,6 +455,8 @@ class FoodDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context["fields"] = [(
Food._meta.get_field(field).verbose_name.capitalize(),
value) for field, value in fields.items()]
if self.object.QR_code.exists():
context["QR_code"] = self.object.QR_code.first()
context["meals"] = self.object.transformed_ingredient_inv.all()
context["update"] = PermissionBackend.check_perm(self.request, "food.change_food")
context["add_ingredient"] = (self.object.end_of_life == '' and PermissionBackend.check_perm(self.request, "food.change_transformedfood"))
@ -506,3 +510,14 @@ class TransformedFoodDetailView(FoodDetailView):
if Food.objects.filter(pk=kwargs['pk']).count() == 1:
kwargs['stop_redirect'] = (Food.objects.get(pk=kwargs['pk']).polymorphic_ctype.model == 'transformedfood')
return super().get(*args, **kwargs)
class QRCodeRedirectView(RedirectView):
"""
Redirects to the QR code creation page from Food List
"""
def get_redirect_url(self, *args, **kwargs):
slug = self.request.GET.get('slug')
if slug:
return reverse_lazy('food:qrcode_create', kwargs={'slug': slug})
return reverse_lazy('food:list')

View File

@ -438,8 +438,6 @@ class Membership(models.Model):
)
if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
new_membership._force_renew_parent = True
if hasattr(self, '_soge') and self._soge:
new_membership._soge = True
if hasattr(self, '_force_save') and self._force_save:
new_membership._force_save = True
new_membership.save()
@ -458,8 +456,6 @@ class Membership(models.Model):
# Renew the previous membership of the parent club
parent_membership = parent_membership.first()
parent_membership._force_renew_parent = True
if hasattr(self, '_soge'):
parent_membership._soge = True
if hasattr(self, '_force_save'):
parent_membership._force_save = True
parent_membership.renew()
@ -471,8 +467,6 @@ class Membership(models.Model):
date_start=self.date_start,
)
parent_membership._force_renew_parent = True
if hasattr(self, '_soge'):
parent_membership._soge = True
if hasattr(self, '_force_save'):
parent_membership._force_save = True
parent_membership.save()

View File

@ -44,7 +44,7 @@ class TemplateLoggedInTests(TestCase):
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL, 302, 302)
def test_logout(self):
response = self.client.get(reverse("logout"))
response = self.client.post(reverse("logout"))
self.assertEqual(response.status_code, 200)
def test_admin_index(self):

View File

@ -13,7 +13,7 @@ def register_note_urls(router, path):
router.register(path + '/note', NotePolymorphicViewSet)
router.register(path + '/alias', AliasViewSet)
router.register(path + '/trust', TrustViewSet)
router.register(path + '/consumer', ConsumerViewSet)
router.register(path + '/consumer', ConsumerViewSet, basename='alias2')
router.register(path + '/transaction/category', TemplateCategoryViewSet)
router.register(path + '/transaction/transaction', TransactionViewSet)

View File

@ -4810,18 +4810,6 @@
]
}
},
{
"model": "permission.role",
"pk": 16,
"fields": {
"for_club": null,
"name": "\u00c9lectron libre (avec perm)",
"permissions": [
22,
84
]
}
},
{
"model": "permission.role",
"pk": 17,
@ -5093,11 +5081,6 @@
"pk": 15,
"fields": {}
},
{
"model": "wei.weirole",
"pk": 16,
"fields": {}
},
{
"model": "wei.weirole",
"pk": 17,

View File

@ -1,8 +1,10 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from oauth2_provider.oauth2_validators import OAuth2Validator
from oauth2_provider.scopes import BaseScopes
from member.models import Club
from note.models import Alias
from note_kfet.middlewares import get_current_request
from .backends import PermissionBackend
@ -16,26 +18,58 @@ class PermissionScopes(BaseScopes):
and can be useful to make queries through the API with limited privileges.
"""
def get_all_scopes(self):
return {f"{p.id}_{club.id}": f"{p.description} (club {club.name})"
for p in Permission.objects.all() for club in Club.objects.all()}
def get_all_scopes(self, **kwargs):
scopes = {}
if 'scopes' in kwargs:
for scope in kwargs['scopes']:
if scope == 'openid':
scopes['openid'] = "OpenID Connect"
else:
p = Permission.objects.get(id=scope.split('_')[0])
club = Club.objects.get(id=scope.split('_')[1])
scopes[scope] = f"{p.description} (club {club.name})"
return scopes
scopes = {f"{p.id}_{club.id}": f"{p.description} (club {club.name})"
for p in Permission.objects.all() for club in Club.objects.all()}
scopes['openid'] = "OpenID Connect"
return scopes
def get_available_scopes(self, application=None, request=None, *args, **kwargs):
if not application:
return []
return [f"{p.id}_{p.membership.club.id}"
for t in Permission.PERMISSION_TYPES
for p in PermissionBackend.get_raw_permissions(get_current_request(), t[0])]
scopes = [f"{p.id}_{p.membership.club.id}"
for t in Permission.PERMISSION_TYPES
for p in PermissionBackend.get_raw_permissions(get_current_request(), t[0])]
scopes.append('openid')
return scopes
def get_default_scopes(self, application=None, request=None, *args, **kwargs):
if not application:
return []
return [f"{p.id}_{p.membership.club.id}"
for p in PermissionBackend.get_raw_permissions(get_current_request(), 'view')]
scopes = [f"{p.id}_{p.membership.club.id}"
for p in PermissionBackend.get_raw_permissions(get_current_request(), 'view')]
scopes.append('openid')
return scopes
class PermissionOAuth2Validator(OAuth2Validator):
oidc_claim_scope = None # fix breaking change of django-oauth-toolkit 2.0.0
oidc_claim_scope = OAuth2Validator.oidc_claim_scope
oidc_claim_scope.update({"name": 'openid',
"normalized_name": 'openid',
"email": 'openid',
})
def get_additional_claims(self, request):
return {
"name": request.user.username,
"normalized_name": Alias.normalize(request.user.username),
"email": request.user.email,
}
def get_discovery_claims(self, request):
claims = super().get_discovery_claims(self)
return claims + ["name", "normalized_name", "email"]
def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
"""
@ -54,6 +88,8 @@ class PermissionOAuth2Validator(OAuth2Validator):
if scope in scopes:
valid_scopes.add(scope)
request.scopes = valid_scopes
if 'openid' in scopes:
valid_scopes.add('openid')
request.scopes = valid_scopes
return valid_scopes

View File

@ -13,12 +13,14 @@ EXCLUDED = [
'cas_server.serviceticket',
'cas_server.user',
'cas_server.userattributes',
'constance.constance',
'contenttypes.contenttype',
'logs.changelog',
'migrations.migration',
'oauth2_provider.accesstoken',
'oauth2_provider.grant',
'oauth2_provider.refreshtoken',
'oauth2_provider.idtoken',
'sessions.session',
]

View File

@ -164,14 +164,24 @@ class ScopesView(LoginRequiredMixin, TemplateView):
from oauth2_provider.models import Application
from .scopes import PermissionScopes
scopes = PermissionScopes()
oidc = False
context["scopes"] = {}
all_scopes = scopes.get_all_scopes()
for app in Application.objects.filter(user=self.request.user).all():
available_scopes = scopes.get_available_scopes(app)
available_scopes = PermissionScopes().get_available_scopes(app)
context["scopes"][app] = OrderedDict()
items = [(k, v) for (k, v) in all_scopes.items() if k in available_scopes]
all_scopes = PermissionScopes().get_all_scopes(scopes=available_scopes)
scopes = {}
for scope in available_scopes:
scopes[scope] = all_scopes[scope]
# remove OIDC scope for sort
if 'openid' in scopes:
del scopes['openid']
oidc = True
items = [(k, v) for (k, v) in scopes.items()]
items.sort(key=lambda x: (int(x[0].split("_")[1]), int(x[0].split("_")[0])))
# add oidc if necessary
if oidc:
items.append(('openid', PermissionScopes().get_all_scopes(scopes=['openid'])['openid']))
for k, v in items:
context["scopes"][app][k] = v

View File

@ -353,7 +353,7 @@ class SogeCredit(models.Model):
def amount(self):
if self.valid:
return self.credit_transaction.total
amount = sum(transaction.total for transaction in self.transactions.all())
amount = sum(max(transaction.total - 2000, 0) for transaction in self.transactions.all())
if 'wei' in settings.INSTALLED_APPS:
from wei.models import WEIMembership
if not WEIMembership.objects\
@ -441,7 +441,7 @@ class SogeCredit(models.Model):
With Great Power Comes Great Responsibility...
"""
total_fee = sum(transaction.total for transaction in self.transactions.all() if not transaction.valid)
total_fee = sum(max(transaction.total - 2000, 0) for transaction in self.transactions.all() if not transaction.valid)
if self.user.note.balance < total_fee:
raise ValidationError(_("This user doesn't have enough money to pay the memberships with its note. "
"Please ask her/him to credit the note before invalidating this credit."))

View File

@ -77,7 +77,7 @@ class WEIRegistrationViewSet(ReadProtectedModelViewSet):
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['user', 'user__username', 'user__first_name', 'user__last_name', 'user__email',
'user__note__alias__name', 'user__note__alias__normalized_name', 'wei', 'wei__name',
'wei__email', 'wei__year', 'soge_credit', 'caution_check', 'birth_date', 'gender',
'wei__email', 'wei__year', 'soge_credit', 'deposit_check', 'birth_date', 'gender',
'clothing_cut', 'clothing_size', 'first_year', 'emergency_contact_name',
'emergency_contact_phone', ]
search_fields = ['$user__username', '$user__first_name', '$user__last_name', '$user__email',

View File

@ -5,7 +5,7 @@ from bootstrap_datepicker_plus.widgets import DatePickerInput
from django import forms
from django.contrib.auth.models import User
from django.db.models import Q
from django.forms import CheckboxSelectMultiple
from django.forms import CheckboxSelectMultiple, RadioSelect
from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial, NoteUser
from note_kfet.inputs import AmountInput, Autocomplete, ColorWidget
@ -24,7 +24,8 @@ class WEIForm(forms.ModelForm):
"membership_end": DatePickerInput(),
"date_start": DatePickerInput(),
"date_end": DatePickerInput(),
"caution_amount": AmountInput(),
"deposit_amount": AmountInput(),
"fee_soge_credit": AmountInput(),
}
@ -43,7 +44,7 @@ class WEIRegistrationForm(forms.ModelForm):
fields = [
'user', 'soge_credit', 'birth_date', 'gender', 'clothing_size',
'health_issues', 'emergency_contact_name', 'emergency_contact_phone',
'first_year', 'information_json', 'caution_check'
'first_year', 'information_json', 'deposit_check'
]
widgets = {
"user": Autocomplete(
@ -58,7 +59,7 @@ class WEIRegistrationForm(forms.ModelForm):
'minDate': '1900-01-01',
'maxDate': '2100-01-01'
}),
"caution_check": forms.BooleanField(
"deposit_check": forms.BooleanField(
required=False,
),
}
@ -66,10 +67,10 @@ class WEIRegistrationForm(forms.ModelForm):
class WEIRegistration2AForm(WEIRegistrationForm):
class Meta(WEIRegistrationForm.Meta):
fields = WEIRegistrationForm.Meta.fields + ['caution_type']
fields = WEIRegistrationForm.Meta.fields + ['deposit_type']
widgets = WEIRegistrationForm.Meta.widgets.copy()
widgets.update({
"caution_type": forms.RadioSelect(),
"deposit_type": forms.RadioSelect(),
})
@ -99,7 +100,7 @@ class WEIChooseBusForm(forms.Form):
queryset=WEIRole.objects.filter(~Q(name="1A") & ~Q(name="GC WEI")),
label=_("WEI Roles"),
help_text=_("Select the roles that you are interested in."),
initial=WEIRole.objects.filter(name="Adhérent⋅e WEI").all(),
initial=WEIRole.objects.filter(Q(name="Adhérent⋅e WEI") | Q(name="\u00c9lectron libre")).all(),
widget=CheckboxSelectMultiple(),
)
@ -140,6 +141,19 @@ class WEIMembershipForm(forms.ModelForm):
required=False,
)
def __init__(self, *args, wei=None, **kwargs):
super().__init__(*args, **kwargs)
if 'bus' in self.fields:
if wei is not None:
self.fields['bus'].queryset = Bus.objects.filter(wei=wei)
else:
self.fields['bus'].queryset = Bus.objects.none()
if 'team' in self.fields:
if wei is not None:
self.fields['team'].queryset = BusTeam.objects.filter(bus__wei=wei)
else:
self.fields['team'].queryset = BusTeam.objects.none()
def clean(self):
cleaned_data = super().clean()
if 'team' in cleaned_data and cleaned_data["team"] is not None \
@ -151,21 +165,8 @@ class WEIMembershipForm(forms.ModelForm):
model = WEIMembership
fields = ('roles', 'bus', 'team',)
widgets = {
"bus": Autocomplete(
Bus,
attrs={
'api_url': '/api/wei/bus/',
'placeholder': 'Bus ...',
}
),
"team": Autocomplete(
BusTeam,
attrs={
'api_url': '/api/wei/team/',
'placeholder': 'Équipe ...',
},
resetable=True,
),
"bus": RadioSelect(),
"team": RadioSelect(),
}
@ -173,7 +174,7 @@ class WEIMembership1AForm(WEIMembershipForm):
"""
Used to confirm registrations of first year members without choosing a bus now.
"""
caution_check = None
deposit_check = None
roles = None
def clean(self):

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.23 on 2025-07-15 14:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wei', '0013_weiclub_caution_amount_weiregistration_caution_type'),
]
operations = [
migrations.AddField(
model_name='weiclub',
name='fee_soge_credit',
field=models.PositiveIntegerField(default=2000, verbose_name='fee soge credit'),
),
]

View File

@ -0,0 +1,40 @@
# Generated by Django 4.2.23 on 2025-07-15 16:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wei', '0014_weiclub_fee_soge_credit'),
]
operations = [
migrations.RemoveField(
model_name='weiclub',
name='caution_amount',
),
migrations.RemoveField(
model_name='weiregistration',
name='caution_check',
),
migrations.RemoveField(
model_name='weiregistration',
name='caution_type',
),
migrations.AddField(
model_name='weiclub',
name='deposit_amount',
field=models.PositiveIntegerField(default=0, verbose_name='deposit amount'),
),
migrations.AddField(
model_name='weiregistration',
name='deposit_check',
field=models.BooleanField(default=False, verbose_name='Deposit check given'),
),
migrations.AddField(
model_name='weiregistration',
name='deposit_type',
field=models.CharField(choices=[('check', 'Check'), ('note', 'Note transaction')], default='check', max_length=16, verbose_name='deposit type'),
),
]

View File

@ -33,11 +33,16 @@ class WEIClub(Club):
verbose_name=_("date end"),
)
caution_amount = models.PositiveIntegerField(
verbose_name=_("caution amount"),
deposit_amount = models.PositiveIntegerField(
verbose_name=_("deposit amount"),
default=0,
)
fee_soge_credit = models.PositiveIntegerField(
verbose_name=_("membership fee (soge credit)"),
default=2000,
)
class Meta:
verbose_name = _("WEI")
verbose_name_plural = _("WEI")
@ -197,19 +202,19 @@ class WEIRegistration(models.Model):
verbose_name=_("Credit from Société générale"),
)
caution_check = models.BooleanField(
deposit_check = models.BooleanField(
default=False,
verbose_name=_("Caution check given")
verbose_name=_("Deposit check given")
)
caution_type = models.CharField(
deposit_type = models.CharField(
max_length=16,
choices=(
('check', _("Check")),
('note', _("Note transaction")),
),
default='check',
verbose_name=_("caution type"),
verbose_name=_("deposit type"),
)
birth_date = models.DateField(
@ -319,7 +324,8 @@ class WEIRegistration(models.Model):
date_start__gte=bde.membership_start,
).exists()
fee = self.wei.membership_fee_paid if self.user.profile.paid \
fee = self.wei.fee_soge_credit if self.soge_credit \
else self.wei.membership_fee_paid if self.user.profile.paid \
else self.wei.membership_fee_unpaid
if not kfet_member:
fee += kfet.membership_fee_paid if self.user.profile.paid \

View File

@ -123,7 +123,7 @@ class WEIRegistrationTable(tables.Table):
}
model = WEIRegistration
template_name = 'django_tables2/bootstrap4.html'
fields = ('user', 'user__first_name', 'user__last_name', 'first_year', 'caution_check',
fields = ('user', 'user__first_name', 'user__last_name', 'first_year', 'deposit_check',
'edit', 'validate', 'delete',)
row_attrs = {
'class': 'table-row',
@ -163,7 +163,7 @@ class WEIMembershipTable(tables.Table):
model = WEIMembership
template_name = 'django_tables2/bootstrap4.html'
fields = ('user', 'user__last_name', 'user__first_name', 'registration__gender', 'user__profile__department',
'year', 'bus', 'team', 'registration__caution_check', )
'year', 'bus', 'team', 'registration__deposit_check', )
row_attrs = {
'class': 'table-row',
'id': lambda record: "row-" + str(record.pk),

View File

@ -49,9 +49,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% 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>
{% if club.deposit_amount > 0 %}
<dt class="col-xl-6">{% trans 'Deposit amount'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.deposit_amount|pretty_money }}</dd>
{% endif %}
{% if "note.view_note"|has_perm:club.note %}

View File

@ -95,8 +95,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
<dd class="col-xl-6"><em>{% trans "The algorithm didn't run." %}</em></dd>
{% endif %}
{% else %}
<dt class="col-xl-6">{% trans 'caution check given'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.caution_check|yesno }}</dd>
<dt class="col-xl-6">{% trans 'Deposit check given'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.deposit_check|yesno }}</dd>
{% with information=registration.information %}
<dt class="col-xl-6">{% trans 'preferred bus'|capfirst %}</dt>
@ -137,41 +137,37 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if registration.soge_credit %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
The WEI will be paid by Société générale. The membership will be created even if the bank didn't pay the BDE yet.
The WEI will partially be paid by Société générale. The membership will be created even if the bank didn't pay the BDE yet.
The membership transaction will be created but will be invalid. You will have to validate it once the bank
validated the creation of the account, or to change the payment method.
{% 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>
{% 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>
{% endif %}
<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.deposit_type == 'note' %}
<li>{% blocktrans trimmed with amount=club.deposit_amount|pretty_money %}
Deposit (by Note transaction): {{ amount }}
{% endblocktrans %}</li>
{% else %}
<li>{% blocktrans trimmed with amount=club.deposit_amount|pretty_money %}
Deposit (by check): {{ amount }}
{% endblocktrans %}</li>
{% endif %}
<li><strong>{% blocktrans trimmed with total=total_needed|pretty_money %}
Total needed: {{ total }}
{% endblocktrans %}</strong></li>
</ul>
<p>{% blocktrans trimmed with balance=registration.user.note.balance|pretty_money %}
Current balance: {{ balance }}
{% endblocktrans %}</p>
</div>
{% if not registration.caution_check and not registration.first_year and registration.caution_type == 'check' %}
{% if not registration.deposit_check and not registration.first_year and registration.caution_type == 'check' %}
<div class="alert alert-danger">
{% trans "The user didn't give her/his caution check." %}
</div>
@ -210,4 +206,27 @@ SPDX-License-Identifier: GPL-3.0-or-later
}
}
</script>
<script>
$(document).ready(function () {
function refreshTeams() {
let buses = [];
$("input[name='bus']:checked").each(function (ignored) {
buses.push($(this).parent().text().trim());
});
console.log(buses);
$("input[name='team']").each(function () {
let label = $(this).parent();
$(this).parent().addClass('d-none');
buses.forEach(function (bus) {
if (label.text().includes(bus))
label.removeClass('d-none');
});
});
}
$("input[name='bus']").change(refreshTeams);
refreshTeams();
});
</script>
{% endblock %}

View File

@ -101,7 +101,7 @@ class TestWEIRegistration(TestCase):
user_id=self.user.id,
wei_id=self.wei.id,
soge_credit=True,
caution_check=True,
deposit_check=True,
birth_date=date(2000, 1, 1),
gender="nonbinary",
clothing_cut="male",
@ -121,12 +121,13 @@ class TestWEIRegistration(TestCase):
email="gc.wei@example.com",
membership_fee_paid=12500,
membership_fee_unpaid=5500,
fee_soge_credit=2000,
membership_start=str(self.year + 1) + "-08-01",
membership_end=str(self.year + 1) + "-09-30",
year=self.year + 1,
date_start=str(self.year + 1) + "-09-01",
date_end=str(self.year + 1) + "-09-03",
caution_amount=12000,
deposit_amount=12000,
))
qs = WEIClub.objects.filter(name="Create WEI Test", year=self.year + 1)
self.assertTrue(qs.exists())
@ -157,11 +158,12 @@ class TestWEIRegistration(TestCase):
email="wei-updated@example.com",
membership_fee_paid=0,
membership_fee_unpaid=0,
fee_soge_credit=0,
membership_start="2000-08-01",
membership_end="2000-09-30",
date_start="2000-09-01",
date_end="2000-09-03",
caution_amount=12000,
deposit_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 +322,7 @@ class TestWEIRegistration(TestCase):
bus=[],
team=[],
roles=[],
caution_type='check'
deposit_type='check'
))
self.assertEqual(response.status_code, 200)
self.assertFalse(response.context["membership_form"].is_valid())
@ -338,7 +340,7 @@ class TestWEIRegistration(TestCase):
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'
deposit_type='check'
))
qs = WEIRegistration.objects.filter(user_id=user.id)
self.assertTrue(qs.exists())
@ -358,7 +360,7 @@ 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'
deposit_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,7 +513,7 @@ 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'
deposit_type='check'
)
)
qs = WEIRegistration.objects.filter(user_id=self.user.id, soge_credit=False, clothing_size="M")
@ -566,7 +568,7 @@ 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'
deposit_type='check'
)
)
qs = WEIRegistration.objects.filter(user_id=self.user.id, clothing_size="L")
@ -590,7 +592,7 @@ class TestWEIRegistration(TestCase):
team=[],
roles=[],
information_json=self.registration.information_json,
caution_type='check'
deposit_type='check'
)
)
self.assertFalse(response.context["membership_form"].is_valid())
@ -640,7 +642,7 @@ class TestWEIRegistration(TestCase):
last_name="admin",
first_name="admin",
bank="Société générale",
caution_check=True,
deposit_check=True,
))
self.assertEqual(response.status_code, 200)
self.assertFalse(response.context["form"].is_valid())
@ -655,7 +657,7 @@ class TestWEIRegistration(TestCase):
last_name="admin",
first_name="admin",
bank="Société générale",
caution_check=True,
deposit_check=True,
))
self.assertRedirects(response, reverse("wei:wei_registrations", kwargs=dict(pk=self.registration.wei.pk)), 302, 200)
@ -678,11 +680,7 @@ class TestWEIRegistration(TestCase):
self.assertTrue(soge_credit.exists())
soge_credit = soge_credit.get()
self.assertTrue(membership.transaction in soge_credit.transactions.all())
self.assertTrue(kfet_membership.transaction in soge_credit.transactions.all())
self.assertTrue(bde_membership.transaction in soge_credit.transactions.all())
self.assertFalse(membership.transaction.valid)
self.assertFalse(kfet_membership.transaction.valid)
self.assertFalse(bde_membership.transaction.valid)
# Check that if the WEI is started, we can't update a wei
self.wei.date_start = date(2000, 1, 1)
@ -815,7 +813,7 @@ class TestWeiAPI(TestAPI):
user_id=self.user.id,
wei_id=self.wei.id,
soge_credit=True,
caution_check=True,
deposit_check=True,
birth_date=date(2000, 1, 1),
gender="nonbinary",
clothing_cut="male",

View File

@ -27,7 +27,7 @@ from django.views.generic.edit import BaseFormView, DeleteView
from django_tables2 import SingleTableView, MultiTableMixin
from api.viewsets import is_regex
from member.models import Membership, Club
from note.models import Transaction, NoteClub, Alias, SpecialTransaction, NoteSpecial
from note.models import Transaction, NoteClub, Alias, SpecialTransaction
from note.tables import HistoryTable
from note_kfet.settings import BASE_DIR
from permission.backends import PermissionBackend
@ -560,13 +560,15 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView):
# 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 "deposit_check" in form.fields:
del form.fields["deposit_check"]
if "information_json" in form.fields:
del form.fields["information_json"]
if "caution_type" in form.fields:
del form.fields["caution_type"]
if "deposit_type" in form.fields:
del form.fields["deposit_type"]
if "soge_credit" in form.fields:
form.fields["soge_credit"].help_text = _('Check if you will open a Société Générale account')
return form
@transaction.atomic
@ -658,6 +660,9 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.fields["user"].initial = self.request.user
if "soge_credit" in form.fields:
form.fields["soge_credit"].help_text = _('Check if you will open a Société Générale account')
if "myself" in self.request.path and self.request.user.profile.soge:
form.fields["soge_credit"].disabled = True
form.fields["soge_credit"].help_text = _("You already opened an account in the Société générale.")
@ -665,16 +670,16 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
# 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 "deposit_check" in form.fields:
del form.fields["deposit_check"]
if "information_json" in form.fields:
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)
# S'assurer que le champ deposit_type est obligatoire
if "deposit_type" in form.fields:
form.fields["deposit_type"].required = True
form.fields["deposit_type"].help_text = _("Choose how you want to pay the deposit")
form.fields["deposit_type"].widget = forms.RadioSelect(choices=form.fields["deposit_type"].choices)
return form
@ -703,7 +708,7 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
form.instance.information = information
# Sauvegarder le type de caution
form.instance.caution_type = form.cleaned_data["caution_type"]
form.instance.deposit_type = form.cleaned_data["deposit_type"]
form.instance.save()
if 'treasury' in settings.INSTALLED_APPS:
@ -773,22 +778,23 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
form = super().get_form(form_class)
form.fields["user"].disabled = True
# 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):
if "information_json" in form.fields:
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"]
# Masquer le champ deposit_check pour tout le monde dans le formulaire de modification
if "deposit_check" in form.fields:
del form.fields["deposit_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)
# S'assurer que le champ deposit_type est obligatoire pour les 2A+
if not self.object.first_year and "deposit_type" in form.fields:
form.fields["deposit_type"].required = True
form.fields["deposit_type"].help_text = _("Choose how you want to pay the deposit")
form.fields["deposit_type"].widget = forms.RadioSelect(choices=form.fields["deposit_type"].choices)
return form
def get_membership_form(self, data=None, instance=None):
membership_form = WEIMembershipForm(data if data else None, instance=instance)
registration = self.get_object()
membership_form = WEIMembershipForm(data if data else None, instance=instance, wei=registration.wei)
del membership_form.fields["credit_type"]
del membership_form.fields["credit_amount"]
del membership_form.fields["first_name"]
@ -844,8 +850,8 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
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"]
if "deposit_type" in form.cleaned_data:
form.instance.deposit_type = form.cleaned_data["deposit_type"]
form.instance.save()
return super().form_valid(form)
@ -951,8 +957,8 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
# Calculer le montant total nécessaire (frais + caution si transaction)
total_needed = fee
if registration.caution_type == 'note':
total_needed += registration.wei.caution_amount
if registration.deposit_type == 'note':
total_needed += registration.wei.deposit_amount
context["total_needed"] = total_needed
form = context["form"]
@ -969,40 +975,38 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
return WEIMembership1AForm
return WEIMembershipForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
wei = registration.wei
kwargs['wei'] = wei
return kwargs
def get_form(self, form_class=None):
form = super().get_form(form_class)
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
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
# Ajouter le champ deposit_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(
if registration.deposit_type == 'check':
form.fields["deposit_check"] = forms.BooleanField(
required=True,
initial=registration.caution_check,
label=_("Caution check given"),
initial=registration.deposit_check,
label=_("Deposit check given"),
help_text=_("Please make sure the check is given before validating the registration")
)
else:
form.fields["caution_check"] = forms.BooleanField(
form.fields["deposit_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
'amount': registration.wei.deposit_amount / 100
}
)
if registration.soge_credit:
form.fields["credit_type"].disabled = True
form.fields["credit_type"].initial = NoteSpecial.objects.get(special_type="Virement bancaire")
form.fields["credit_amount"].disabled = True
form.fields["last_name"].disabled = True
form.fields["first_name"].disabled = True
form.fields["bank"].disabled = True
form.fields["bank"].initial = "Société générale"
if 'bus' in form.fields:
# For 2A+ and hardcoded 1A
form.fields["bus"].widget.attrs["api_url"] = "/api/wei/bus/?wei=" + str(registration.wei.pk)
@ -1035,8 +1039,8 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
club = registration.wei
user = registration.user
if "caution_check" in form.data:
registration.caution_check = form.data["caution_check"] == "on"
if "deposit_check" in form.data:
registration.deposit_check = form.data["deposit_check"] == "on"
registration.save()
membership = form.instance
membership.user = user
@ -1047,6 +1051,8 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
membership._force_renew_parent = True
fee = club.membership_fee_paid if user.profile.paid else club.membership_fee_unpaid
if registration.soge_credit:
fee = 2000
kfet = club.parent_club
bde = kfet.parent_club
@ -1073,16 +1079,16 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
first_name = form.cleaned_data["first_name"]
bank = form.cleaned_data["bank"]
if credit_type is None or registration.soge_credit:
if credit_type is None:
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
if registration.deposit_type == 'note':
total_needed += club.deposit_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 user.note.balance + credit_amount < total_needed:
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") % {
@ -1130,14 +1136,14 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
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':
if registration.deposit_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},
amount=club.deposit_amount,
reason=_("Deposit %(name)s") % {'name': club.name},
valid=True,
)